From 5816ec0d38befead7c7db47f1d9ca32314fa10c2 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sat, 15 Dec 2018 20:55:32 +0000 Subject: [PATCH 01/14] Fix end-call flow to avoid unknown UUID error --- ios/RNTwilioVoice/RNTwilioVoice.m | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ios/RNTwilioVoice/RNTwilioVoice.m b/ios/RNTwilioVoice/RNTwilioVoice.m index f7d725fa..2227713c 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.m +++ b/ios/RNTwilioVoice/RNTwilioVoice.m @@ -99,7 +99,7 @@ - (void)dealloc { device.proximityMonitoringEnabled = YES; if (self.call && self.call.state == TVOCallStateConnected) { - [self.call disconnect]; + [self performEndCallActionWithUUID:self.call.uuid]; } else { NSUUID *uuid = [NSUUID UUID]; NSString *handle = [params valueForKey:@"To"]; @@ -357,7 +357,6 @@ - (void)call:(TVOCall *)call didFailToConnectWithError:(NSError *)error { - (void)call:(TVOCall *)call didDisconnectWithError:(NSError *)error { NSLog(@"Call disconnected with error: %@", error); - [self performEndCallActionWithUUID:call.uuid]; [self callDisconnected:error]; } From 1be0b4abfa81c7d50c64a2a232377abe08586dca Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sat, 15 Dec 2018 20:57:49 +0000 Subject: [PATCH 02/14] fix: align iOS params to Android fixed connectionDidConnect, connectionDidDisconnect. fixes #44 #62 --- ios/RNTwilioVoice/RNTwilioVoice.m | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/ios/RNTwilioVoice/RNTwilioVoice.m b/ios/RNTwilioVoice/RNTwilioVoice.m index 2227713c..50f8e93a 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.m +++ b/ios/RNTwilioVoice/RNTwilioVoice.m @@ -303,11 +303,11 @@ - (void)handleCallInviteCanceled:(TVOCallInvite *)callInvite { [params setObject:self.callInvite.callSid forKey:@"call_sid"]; } - if (self.callInvite.from){ - [params setObject:self.callInvite.from forKey:@"from"]; + if (self.callInvite.from) { + [params setObject:self.callInvite.from forKey:@"call_from"]; } - if (self.callInvite.to){ - [params setObject:self.callInvite.to forKey:@"to"]; + if (self.callInvite.to) { + [params setObject:self.callInvite.to forKey:@"call_to"]; } if (self.callInvite.state == TVOCallInviteStateCanceled) { [params setObject:StateDisconnected forKey:@"call_state"]; @@ -337,11 +337,11 @@ - (void)callDidConnect:(TVOCall *)call { [callParams setObject:StateConnected forKey:@"call_state"]; } - if (call.from){ - [callParams setObject:call.from forKey:@"from"]; + if (call.from) { + [callParams setObject:call.from forKey:@"call_from"]; } - if (call.to){ - [callParams setObject:call.to forKey:@"to"]; + if (call.to) { + [callParams setObject:call.to forKey:@"call_to"]; } [self sendEventWithName:@"connectionDidConnect" body:callParams]; } @@ -367,12 +367,11 @@ - (void)callDisconnected:(NSError *)error { if (error.localizedFailureReason) { errMsg = [error localizedFailureReason]; } - [params setObject:errMsg forKey:@"error"]; + [params setObject:errMsg forKey:@"err"]; } if (self.call.sid) { [params setObject:self.call.sid forKey:@"call_sid"]; } - if (self.call.to){ [params setObject:self.call.to forKey:@"call_to"]; } if (self.call.from){ From c95b559b274dc2609e2e8dbe87bbbe62e387b2d1 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sat, 15 Dec 2018 20:58:14 +0000 Subject: [PATCH 03/14] update: android to com.twilio:voice-android:2.0.9 --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index 878fc179..880ff171 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -44,7 +44,7 @@ android { dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') - compile 'com.twilio:voice-android:2.0.7' + compile 'com.twilio:voice-android:2.0.9' compile 'com.android.support:appcompat-v7:27.0.2' compile 'com.facebook.react:react-native:+' compile 'com.google.firebase:firebase-messaging:17.+' From c5406f887ba845da1f9bd433eccb72aa13f1ed6d Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sat, 15 Dec 2018 21:00:39 +0000 Subject: [PATCH 04/14] update: Android firebase-messaging to 17.3.4 --- android/build.gradle | 2 +- .../RNTwilioVoice/TwilioVoiceModule.java | 63 +++++++++++-------- .../fcm/VoiceFirebaseInstanceIDService.java | 43 ------------- .../fcm/VoiceFirebaseMessagingService.java | 11 +++- 4 files changed, 49 insertions(+), 70 deletions(-) delete mode 100644 android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseInstanceIDService.java diff --git a/android/build.gradle b/android/build.gradle index 880ff171..e2b65ac7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -47,6 +47,6 @@ dependencies { compile 'com.twilio:voice-android:2.0.9' compile 'com.android.support:appcompat-v7:27.0.2' compile 'com.facebook.react:react-native:+' - compile 'com.google.firebase:firebase-messaging:17.+' + compile 'com.google.firebase:firebase-messaging:17.3.4' testCompile 'junit:junit:4.12' } diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java index 774ad936..228b4b63 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java @@ -15,6 +15,7 @@ import android.media.AudioManager; import android.os.Build; +import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v4.content.LocalBroadcastManager; @@ -38,8 +39,10 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; -import com.google.firebase.FirebaseApp; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; import com.twilio.voice.Call; import com.twilio.voice.CallException; import com.twilio.voice.CallInvite; @@ -494,15 +497,16 @@ public void initWithAccessToken(final String accessToken, Promise promise) { if (accessToken.equals("")) { promise.reject(new JSApplicationIllegalArgumentException("Invalid access token")); return; - } - + } + if(!checkPermissionForMicrophone()) { promise.reject(new AssertionException("Can't init without microphone permission")); - } + return; + } TwilioVoiceModule.this.accessToken = accessToken; if (BuildConfig.DEBUG) { - Log.d(TAG, "initWithAccessToken ACTION_FCM_TOKEN"); + Log.d(TAG, "initWithAccessToken"); } registerForCallInvites(); WritableMap params = Arguments.createMap(); @@ -533,20 +537,28 @@ private void clearIncomingNotification(CallInvite callInvite) { * If a valid google-services.json has not been provided or the FirebaseInstanceId has not been * initialized the fcmToken will be null. * - * In the case where the FirebaseInstanceId has not yet been initialized the - * VoiceFirebaseInstanceIDService.onTokenRefresh should result in a LocalBroadcast to this - * activity which will attempt registerForCallInvites again. - * */ private void registerForCallInvites() { - FirebaseApp.initializeApp(getReactApplicationContext()); - final String fcmToken = FirebaseInstanceId.getInstance().getToken(); - if (fcmToken != null) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "Registering with FCM"); - } - Voice.register(getReactApplicationContext(), accessToken, Voice.RegistrationChannel.FCM, fcmToken, registrationListener); - } + FirebaseInstanceId.getInstance().getInstanceId() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (!task.isSuccessful()) { + Log.w(TAG, "getInstanceId failed", task.getException()); + return; + } + + // Get new Instance ID token + String fcmToken = task.getResult().getToken(); + if (fcmToken != null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Registering with FCM"); + } + Voice.register(getReactApplicationContext(), accessToken, Voice.RegistrationChannel.FCM, fcmToken, registrationListener); + } + } + }); + } @ReactMethod @@ -778,14 +790,15 @@ private boolean checkPermissionForMicrophone() { } private void requestPermissionForMicrophone() { - if (getCurrentActivity() != null) { - if (ActivityCompat.shouldShowRequestPermissionRationale(getCurrentActivity(), Manifest.permission.RECORD_AUDIO)) { - // Snackbar.make(coordinatorLayout, - // "Microphone permissions needed. Please allow in your application settings.", - // SNACKBAR_DURATION).show(); - } else { - ActivityCompat.requestPermissions(getCurrentActivity(), new String[]{Manifest.permission.RECORD_AUDIO}, MIC_PERMISSION_REQUEST_CODE); - } + if (getCurrentActivity() == null) { + return; + } + if (ActivityCompat.shouldShowRequestPermissionRationale(getCurrentActivity(), Manifest.permission.RECORD_AUDIO)) { +// Snackbar.make(coordinatorLayout, +// "Microphone permissions needed. Please allow in your application settings.", +// SNACKBAR_DURATION).show(); + } else { + ActivityCompat.requestPermissions(getCurrentActivity(), new String[]{Manifest.permission.RECORD_AUDIO}, MIC_PERMISSION_REQUEST_CODE); } } } diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseInstanceIDService.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseInstanceIDService.java deleted file mode 100644 index 9154661c..00000000 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseInstanceIDService.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.hoxfon.react.RNTwilioVoice.fcm; - -import android.content.Intent; -import android.support.v4.content.LocalBroadcastManager; -import android.util.Log; - -import com.google.firebase.iid.FirebaseInstanceId; -import com.google.firebase.iid.FirebaseInstanceIdService; -import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_FCM_TOKEN; -import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG; - -public class VoiceFirebaseInstanceIDService extends FirebaseInstanceIdService { - - /** - * Called if InstanceID token is updated. This may occur if the security of - * the previous token had been compromised. Note that this is called when the InstanceID token - * is initially generated so this is where you would retrieve the token. - */ - // [START refresh_token] - @Override - public void onTokenRefresh() { - // Get updated InstanceID token. - String refreshedToken = FirebaseInstanceId.getInstance().getToken(); - Log.d(TAG, "Refreshed token: " + refreshedToken); - - // Notify Activity of FCM token - Intent intent = new Intent(ACTION_FCM_TOKEN); - LocalBroadcastManager.getInstance(this).sendBroadcast(intent); - } - // [END refresh_token] - - /** - * Persist token to third-party servers. - * - * Modify this method to associate the user's FCM InstanceID token with any server-side account - * maintained by your application. - * - * @param token The new token. - */ - private void sendRegistrationToServer(String token) { - // TODO: Implement this method to send token to your app server. - } -} diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java index b50facdc..bb4e6d6f 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java @@ -4,7 +4,6 @@ import android.app.ActivityManager; import android.content.Intent; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.support.v4.content.LocalBroadcastManager; @@ -28,6 +27,7 @@ import java.util.Random; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_FCM_TOKEN; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_INCOMING_CALL; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_INVITE; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_NOTIFICATION_ID; @@ -43,6 +43,15 @@ public void onCreate() { callNotificationManager = new CallNotificationManager(); } + @Override + public void onNewToken(String token) { + Log.d(TAG, "Refreshed token: " + token); + + // Notify Activity of FCM token + Intent intent = new Intent(ACTION_FCM_TOKEN); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + /** * Called when message is received. * From b98cc7fd6eb4f4edcae11e8e726d84a512265bdf Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sat, 15 Dec 2018 21:01:46 +0000 Subject: [PATCH 05/14] formatting --- ios/RNTwilioVoice/RNTwilioVoice.m | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/ios/RNTwilioVoice/RNTwilioVoice.m b/ios/RNTwilioVoice/RNTwilioVoice.m index 50f8e93a..7fc54618 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.m +++ b/ios/RNTwilioVoice/RNTwilioVoice.m @@ -122,14 +122,14 @@ - (void)dealloc { [self toggleAudioRoute:speaker]; } -RCT_EXPORT_METHOD(sendDigits: (NSString *)digits){ +RCT_EXPORT_METHOD(sendDigits: (NSString *)digits) { if (self.call && self.call.state == TVOCallStateConnected) { NSLog(@"SendDigits %@", digits); [self.call sendDigits:digits]; } } -RCT_EXPORT_METHOD(unregister){ +RCT_EXPORT_METHOD(unregister) { NSLog(@"unregister"); NSString *accessToken = [self fetchAccessToken]; @@ -148,16 +148,16 @@ - (void)dealloc { RCT_REMAP_METHOD(getActiveCall, resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject){ + rejecter:(RCTPromiseRejectBlock)reject) { NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; if (self.callInvite) { - if (self.callInvite.callSid){ + if (self.callInvite.callSid) { [params setObject:self.callInvite.callSid forKey:@"call_sid"]; } - if (self.callInvite.from){ + if (self.callInvite.from) { [params setObject:self.callInvite.from forKey:@"from"]; } - if (self.callInvite.to){ + if (self.callInvite.to) { [params setObject:self.callInvite.to forKey:@"to"]; } if (self.callInvite.state == TVOCallInviteStatePending) { @@ -172,10 +172,10 @@ - (void)dealloc { if (self.call.sid) { [params setObject:self.call.sid forKey:@"call_sid"]; } - if (self.call.to){ + if (self.call.to) { [params setObject:self.call.to forKey:@"call_to"]; } - if (self.call.from){ + if (self.call.from) { [params setObject:self.call.from forKey:@"call_from"]; } if (self.call.state == TVOCallStateConnected) { @@ -299,7 +299,7 @@ - (void)handleCallInviteCanceled:(TVOCallInvite *)callInvite { [self performEndCallActionWithUUID:callInvite.uuid]; NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - if (self.callInvite.callSid){ + if (self.callInvite.callSid) { [params setObject:self.callInvite.callSid forKey:@"call_sid"]; } @@ -372,9 +372,10 @@ - (void)callDisconnected:(NSError *)error { if (self.call.sid) { [params setObject:self.call.sid forKey:@"call_sid"]; } + if (self.call.to) { [params setObject:self.call.to forKey:@"call_to"]; } - if (self.call.from){ + if (self.call.from) { [params setObject:self.call.from forKey:@"call_from"]; } if (self.call.state == TVOCallStateDisconnected) { From 4c7386d4b3049533000f2dfb720630080a87ee3d Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Thu, 27 Dec 2018 18:37:47 +0000 Subject: [PATCH 06/14] fix: align lib dependency with root update build.gradle as per recommendations in RN 0.57.8 update gradle to v 3.2.1 --- android/build.gradle | 30 +++++++++++++++++++++--------- android/settings.gradle | 0 2 files changed, 21 insertions(+), 9 deletions(-) delete mode 100644 android/settings.gradle diff --git a/android/build.gradle b/android/build.gradle index e2b65ac7..c2cd1d1f 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,11 +2,15 @@ buildscript { repositories { + maven { + url 'https://maven.google.com/' + name 'Google' + } jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.1' - classpath 'com.google.gms:google-services:3.1.2' + classpath 'com.android.tools.build:gradle:3.2.1' + classpath 'com.google.gms:google-services:4.2.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -15,21 +19,27 @@ buildscript { allprojects { repositories { - jcenter() maven { - url "https://maven.google.com" + url 'https://maven.google.com/' + name 'Google' } + jcenter() } } apply plugin: 'com.android.library' +def DEFAULT_COMPILE_SDK_VERSION = 27 +def DEFAULT_BUILD_TOOLS_VERSION = "27.0.3" +def DEFAULT_TARGET_SDK_VERSION = 27 +def DEFAULT_SUPPORT_LIB_VERSION = "27.0.2" + android { - compileSdkVersion 27 - buildToolsVersion "27.0.3" + compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION + buildToolsVersion rootProject.hasProperty('buildToolsVersion') ? rootProject.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION defaultConfig { minSdkVersion 16 - targetSdkVersion 27 + targetSdkVersion rootProject.hasProperty('targetSdkVersion') ? rootProject.targetSdkVersion : DEFAULT_TARGET_SDK_VERSION versionCode 1 versionName "1.0" vectorDrawables.useSupportLibrary = true @@ -43,10 +53,12 @@ android { } dependencies { + def supportLibVersion = rootProject.hasProperty('supportLibVersion') ? rootProject.supportLibVersion : DEFAULT_SUPPORT_LIB_VERSION + compile fileTree(include: ['*.jar'], dir: 'libs') compile 'com.twilio:voice-android:2.0.9' - compile 'com.android.support:appcompat-v7:27.0.2' + compile 'com.android.support:appcompat-v7:$supportLibVersion' compile 'com.facebook.react:react-native:+' - compile 'com.google.firebase:firebase-messaging:17.3.4' + compile 'com.google.firebase:firebase-messaging:17.+' testCompile 'junit:junit:4.12' } diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index e69de29b..00000000 From a3b02489c7305b0aa775573c4bcdd3f19005ad40 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sat, 15 Dec 2018 21:01:59 +0000 Subject: [PATCH 07/14] v4.0.0 --- CHANGELOG.md | 8 +++ README.md | 55 ++++++------------- .../RNTwilioVoice/TwilioVoiceModule.java | 2 +- package.json | 2 +- 4 files changed, 27 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bbd175d..484d9f42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ [Release Section](https://github.com/hoxfon/react-native-twilio-programmable-voice/releases) +## 4.0.0 + +- Android + - update Firebase Messaging to 17.3.4 which simplifies how to obtain the FCM token +- iOS + - convert params for connectionDidConnect to => call_to, from => call_from + - convert params for connectionDidDisconnect to => call_to, from => call_from, error => err + ## 3.21.3 - iOS: Upgrade TwilioVoice pod to version 2.1 diff --git a/README.md b/README.md index 95b66906..f3492083 100644 --- a/README.md +++ b/README.md @@ -3,41 +3,15 @@ This is a React Native wrapper for Twilio Programmable Voice SDK that lets you m # Twilio Programmable Voice SDK -- Android 2.0.7 (bundled within this library) -- iOS 2.0.4 (specified by the app's own podfile) +- Android 2.0.9 (bundled within this library) +- iOS 2.0.7 (specified by the app's own podfile) -## Breaking changes in v3.0.0 - -- initWitToken returns an object with a property `initialized` instead of `initilized` -- iOS event `connectionDidConnect` returns the same properties as Android -move property `to` => `call_to` -move property `from` => `call_from` -## Migrating Android from v1 to v2 (incoming call use FCM) -You will need to make changes both on your Twilio account using Twilio Web Console and on your react native app. -Twilio Programmable Voice Android SDK uses `FCM` since version 2.0.0.beta5. - -Before you start, I strongly suggest that you read the list of Twilio changes from Android SDK v2.0.0 beta4 to beta5: -[Twilio example App: Migrating from GCM to FCM](https://github.com/twilio/voice-quickstart-android/blob/d7d4f0658e145eb94ab8f5e34f6fd17314e7ab17/README.md#migrating-from-gcm-to-fcm) - -These are all the changes required: - -- remove all the GCM related code from your `AndroidManifest.xml` and add the following code to receive `FCM` notifications -(I wasn't successful in keeping react-native-fcm working at the same time. If you know how please open an issue to share). +## Breaking changes in v4.0.0 +- Android: remove the following block from your application's `AndroidManifest.xml` ```xml - ..... - - - - - - - - - - ``` -- log into your Firebase console. Navigate to: Project settings > CLOUD MESSAGING. Copy your `Server key` -- in Twilio console add a new Push Credential, type `FCM`, fcm secret Firebase FCM `Server key` -- include in your project `google-services.json`; if you have not include it yet -- rename getIncomingCall() to getActiveCall() +- iOS: params changes for `connectionDidConnect` and `connectionDidDisconnect` + + to => call_to + from => call_from + error => err -If something doesn't work as expected or you want to make a request open an issue. +## Breaking changes in v3.0.0 + +- initWitToken returns an object with a property `initialized` instead of `initilized` +- iOS event `connectionDidConnect` returns the same properties as Android +move property `to` => `call_to` +move property `from` => `call_from` ## Help wanted! @@ -220,8 +199,8 @@ public class MainApplication extends Application implements ReactApplication { protected List getPackages() { return Arrays.asList( new MainReactPackage(), - new TwilioVoicePackage() // <---- Add the Package : by default it will ask microphone permissions - // new TwilioVoicePackage(false) // <---- pass false to handle microphone permissions in your application + new TwilioVoicePackage() // <---- Add the package + // new TwilioVoicePackage(false) // <---- pass false if you don't want to ask for microphone permissions ); } }; diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java index 228b4b63..17409af8 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java @@ -500,7 +500,7 @@ public void initWithAccessToken(final String accessToken, Promise promise) { } if(!checkPermissionForMicrophone()) { - promise.reject(new AssertionException("Can't init without microphone permission")); + promise.reject(new AssertionException("Allow microphone permission")); return; } diff --git a/package.json b/package.json index 30fc16e1..769eeede 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-twilio-programmable-voice", - "version": "3.21.3", + "version": "4.0.0", "description": "React Native wrapper for Twilio Programmable Voice SDK", "main": "index.js", "scripts": { From f86376fea478befa998fcb9c6856e004d980eb62 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Wed, 4 Dec 2019 21:07:35 +0000 Subject: [PATCH 08/14] Android X migration --- .../react/RNTwilioVoice/CallNotificationManager.java | 2 +- .../java/com/hoxfon/react/RNTwilioVoice/EventManager.java | 2 +- .../com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java | 8 ++++---- .../RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java index bace1249..6bf9bcdc 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java @@ -15,7 +15,7 @@ import android.os.Build; import android.os.Bundle; import android.service.notification.StatusBarNotification; -import android.support.v4.app.NotificationCompat; +import androidx.core.app.NotificationCompat; import android.util.Log; import android.view.WindowManager; diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java index 800ef6db..af7a98aa 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java @@ -1,6 +1,6 @@ package com.hoxfon.react.RNTwilioVoice; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.util.Log; import com.facebook.react.bridge.ReactApplicationContext; diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java index 17409af8..64a2b54e 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java @@ -15,10 +15,10 @@ import android.media.AudioManager; import android.os.Build; -import android.support.annotation.NonNull; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; -import android.support.v4.content.LocalBroadcastManager; +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import android.util.Log; import android.view.Window; import android.view.WindowManager; diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java index bb4e6d6f..ca5d4fed 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java @@ -6,7 +6,7 @@ import android.content.Intent; import android.os.Handler; import android.os.Looper; -import android.support.v4.content.LocalBroadcastManager; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import android.util.Log; import com.facebook.react.ReactApplication; From a76215ba55f0ecf115cd151f1f25a365fa8c48ce Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Wed, 4 Dec 2019 21:08:09 +0000 Subject: [PATCH 09/14] Android: fix google maven --- android/build.gradle | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index c2cd1d1f..e147243b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,10 +2,7 @@ buildscript { repositories { - maven { - url 'https://maven.google.com/' - name 'Google' - } + google() jcenter() } dependencies { @@ -19,10 +16,7 @@ buildscript { allprojects { repositories { - maven { - url 'https://maven.google.com/' - name 'Google' - } + google() jcenter() } } From ffc57f25937c37c2e2348ffb75c9888bfb529175 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Wed, 4 Dec 2019 21:08:58 +0000 Subject: [PATCH 10/14] Android: use gradle 5.4.1 --- android/build.gradle | 14 +++++++------- android/gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index e147243b..f2f0c7a1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,7 +6,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' + classpath 'com.android.tools.build:gradle:3.5.2' classpath 'com.google.gms:google-services:4.2.0' // NOTE: Do not place your application dependencies here; they belong @@ -49,10 +49,10 @@ android { dependencies { def supportLibVersion = rootProject.hasProperty('supportLibVersion') ? rootProject.supportLibVersion : DEFAULT_SUPPORT_LIB_VERSION - compile fileTree(include: ['*.jar'], dir: 'libs') - compile 'com.twilio:voice-android:2.0.9' - compile 'com.android.support:appcompat-v7:$supportLibVersion' - compile 'com.facebook.react:react-native:+' - compile 'com.google.firebase:firebase-messaging:17.+' - testCompile 'junit:junit:4.12' + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'com.twilio:voice-android:2.0.9' + implementation "com.android.support:appcompat-v7:$supportLibVersion" + implementation 'com.facebook.react:react-native:+' + implementation 'com.google.firebase:firebase-messaging:17.+' + testImplementation 'junit:junit:4.12' } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 04e285f3..f6405f2b 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip From 45736f48e283df8db7572f040cac6d996522b64b Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Wed, 4 Dec 2019 21:09:17 +0000 Subject: [PATCH 11/14] Android: use API 28 --- android/build.gradle | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index f2f0c7a1..094cc639 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -23,10 +23,10 @@ allprojects { apply plugin: 'com.android.library' -def DEFAULT_COMPILE_SDK_VERSION = 27 -def DEFAULT_BUILD_TOOLS_VERSION = "27.0.3" -def DEFAULT_TARGET_SDK_VERSION = 27 -def DEFAULT_SUPPORT_LIB_VERSION = "27.0.2" +def DEFAULT_COMPILE_SDK_VERSION = 28 +def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3" +def DEFAULT_TARGET_SDK_VERSION = 28 +def DEFAULT_SUPPORT_LIB_VERSION = "28.0.0" android { compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION From d6235bc472a2f7f186226d9f9b4ca65e2f36ffa8 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Wed, 4 Dec 2019 21:31:29 +0000 Subject: [PATCH 12/14] Android: upgrade com.twilio:voice-android to 2.1.0 --- CHANGELOG.md | 10 +++++++--- README.md | 6 ++---- android/build.gradle | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 484d9f42..3004414e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,14 @@ ## 4.0.0 - Android - - update Firebase Messaging to 17.3.4 which simplifies how to obtain the FCM token + - update Firebase Messaging to 17.3.4 which simplifies how to obtain the FCM token + - Android X migration + - use gradle 5.4.1 + - use API 28 + - upgrade com.twilio:voice-android to 2.1.0 - iOS - - convert params for connectionDidConnect to => call_to, from => call_from - - convert params for connectionDidDisconnect to => call_to, from => call_from, error => err + - convert params for connectionDidConnect to => call_to, from => call_from + - convert params for connectionDidDisconnect to => call_to, from => call_from, error => err ## 3.21.3 diff --git a/README.md b/README.md index f3492083..071fd5bc 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,8 @@ This is a React Native wrapper for Twilio Programmable Voice SDK that lets you m # Twilio Programmable Voice SDK -- Android 2.0.9 (bundled within this library) -- iOS 2.0.7 (specified by the app's own podfile) - - +- Android 2.1.0 (bundled within this library) +- iOS 2.1.0 (specified by the app's own podfile) ## Breaking changes in v4.0.0 diff --git a/android/build.gradle b/android/build.gradle index 094cc639..b4365da1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -50,7 +50,7 @@ dependencies { def supportLibVersion = rootProject.hasProperty('supportLibVersion') ? rootProject.supportLibVersion : DEFAULT_SUPPORT_LIB_VERSION implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.twilio:voice-android:2.0.9' + implementation 'com.twilio:voice-android:2.1.0' implementation "com.android.support:appcompat-v7:$supportLibVersion" implementation 'com.facebook.react:react-native:+' implementation 'com.google.firebase:firebase-messaging:17.+' From a2e2c334ef4b31b5d93ca5ed464b35329aec6573 Mon Sep 17 00:00:00 2001 From: Craig Martin Date: Fri, 17 Jan 2020 20:12:44 -0500 Subject: [PATCH 13/14] Incorporate Open PRs --- README.md | 35 +- RNTwilioVoice.podspec | 2 +- android/build.gradle | 2 +- android/gradle.properties | 2 + .../CallNotificationManager.java | 5 +- .../RNTwilioVoice/TwilioVoiceModule.java | 242 ++++++----- .../fcm/VoiceFirebaseMessagingService.java | 50 ++- ios/RNTwilioVoice/RNTwilioVoice.m | 404 +++++++++++------- 8 files changed, 446 insertions(+), 296 deletions(-) diff --git a/README.md b/README.md index 56604f77..f2cd4ade 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # react-native-twilio-programmable-voice -This is a React Native wrapper for Twilio Programmable Voice SDK that lets you make and receive calls from your ReactNatvie App. This module is not curated nor maintained, but inspired by Twilio. +This is a React Native wrapper for Twilio Programmable Voice SDK that lets you make and receive calls from your React Native App. This module is not affiliated with or maintained by the Twilio team. This is maintained by contributions from the community. # Twilio Programmable Voice SDK -- Android 2.1.0 (bundled within this library) -- iOS 2.1.0 (specified by the app's own podfile) +- Android 5.0.2 (bundled within this library) +- iOS 5.1.1 (specified by the app's own podfile; min. version 5.x) ## Breaking changes in v4.0.0 @@ -45,7 +45,7 @@ ReactNative success is directly linked to its module ecosystem. One way to make ## Installation Before starting, we recommend you get familiar with [Twilio Programmable Voice SDK](https://www.twilio.com/docs/api/voice-sdk). -It's easier to integrate this module into your react-native app if you follow the Quick start tutorial from Twilio, because it makes very clear which setup steps are required. +It's easier to integrate this module into your react-native app if you follow the Quick start tutorial from Twilio, because it makes very clear which setup steps are required. On RN 0.60+, this module can be auto-linked (Android still requires FCM setup below). ``` @@ -63,11 +63,11 @@ Edit your `Podfile` to include TwilioVoice framework source 'https://github.com/cocoapods/specs' # min version for TwilioVoice to work -platform :ios, '8.1' +platform :ios, '10.0' target do ... - pod 'TwilioVoice', '~> 2.1.0' + pod 'TwilioVoice', '~> 5.1.1' ... end @@ -82,11 +82,11 @@ Edit your `Podfile` to include TwilioVoice and RNTwilioVoice frameworks source 'https://github.com/cocoapods/specs' # min version for TwilioVoice to work -platform :ios, '8.1' +platform :ios, '10.0' target do ... - pod 'TwilioVoice', '~> 2.1.0' + pod 'TwilioVoice', '~> 5.1.1' pod 'RNTwilioVoice', path: '../node_modules/react-native-twilio-programmable-voice' ... end @@ -120,7 +120,7 @@ It contains keys and settings for all your applications under Firebase. This lib buildscript { ... dependencies { - classpath 'com.google.gms:google-services:3.1.2' + classpath 'com.google.gms:google-services:4.2.0' } } @@ -128,8 +128,9 @@ buildscript { dependencies { ... + // on React Native 0.60+, this module can be auto-linked and doesn't need a manual entry here - compile project(':react-native-twilio-programmable-voice') + implementation project(':react-native-twilio-programmable-voice') } // this plugin looks for google-services.json in your project @@ -156,22 +157,12 @@ In your `AndroidManifest.xml` - - - - - - - - ..... ``` -In `android/settings.gradle` +In `android/settings.gradle` (not necessary if auto-linking on RN 0.60+) ```gradle ... @@ -180,7 +171,7 @@ include ':react-native-twilio-programmable-voice' project(':react-native-twilio-programmable-voice').projectDir = file('../node_modules/react-native-twilio-programmable-voice/android') ``` -Register module (in `MainApplication.java`) +Register module (in `MainApplication.java`) (not necessary if auto-linking on RN 0.60+ unless you want to control microphone permission) ```java import com.hoxfon.react.RNTwilioVoice.TwilioVoicePackage; // <--- Import Package diff --git a/RNTwilioVoice.podspec b/RNTwilioVoice.podspec index 9b80fac6..9a9aac92 100644 --- a/RNTwilioVoice.podspec +++ b/RNTwilioVoice.podspec @@ -9,7 +9,7 @@ Pod::Spec.new do |s| s.authors = spec['author']['name'] s.homepage = spec['homepage'] s.license = spec['license'] - s.platform = :ios, "8.1" + s.platform = :ios, "10.0" s.source_files = [ "ios/RNTwilioVoice/*.h", "ios/RNTwilioVoice/*.m"] s.source = {:path => "./RNTwilioVoice"} diff --git a/android/build.gradle b/android/build.gradle index b4365da1..faa49783 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -50,7 +50,7 @@ dependencies { def supportLibVersion = rootProject.hasProperty('supportLibVersion') ? rootProject.supportLibVersion : DEFAULT_SUPPORT_LIB_VERSION implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.twilio:voice-android:2.1.0' + implementation 'com.twilio:voice-android:5.0.2' implementation "com.android.support:appcompat-v7:$supportLibVersion" implementation 'com.facebook.react:react-native:+' implementation 'com.google.firebase:firebase-messaging:17.+' diff --git a/android/gradle.properties b/android/gradle.properties index aac7c9b4..af6dcbe4 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -15,3 +15,5 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java index 6bf9bcdc..eb861191 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java @@ -21,6 +21,7 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.twilio.voice.CallInvite; +import com.twilio.voice.CancelledCallInvite; import java.util.List; @@ -307,12 +308,12 @@ public void createHangupLocalNotification(ReactApplicationContext context, Strin } public void removeIncomingCallNotification(ReactApplicationContext context, - CallInvite callInvite, + CancelledCallInvite callInvite, int notificationId) { Log.d(TAG, "removeIncomingCallNotification"); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - if (callInvite != null && callInvite.getState() == CallInvite.State.PENDING) { + if (callInvite != null) { /* * If the incoming call message was cancelled then remove the notification by matching * it with the call sid from the list of notifications in the notification drawer. diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java index 64a2b54e..25c4f7b2 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java @@ -46,6 +46,7 @@ import com.twilio.voice.Call; import com.twilio.voice.CallException; import com.twilio.voice.CallInvite; +import com.twilio.voice.ConnectOptions; import com.twilio.voice.LogLevel; import com.twilio.voice.RegistrationException; import com.twilio.voice.RegistrationListener; @@ -78,8 +79,10 @@ public class TwilioVoiceModule extends ReactContextBaseJavaModule implements Act public static final String INCOMING_CALL_INVITE = "INCOMING_CALL_INVITE"; public static final String INCOMING_CALL_NOTIFICATION_ID = "INCOMING_CALL_NOTIFICATION_ID"; public static final String NOTIFICATION_TYPE = "NOTIFICATION_TYPE"; + public static final String CANCELLED_CALL_INVITE = "CANCELLED_CALL_INVITE"; public static final String ACTION_INCOMING_CALL = "com.hoxfon.react.TwilioVoice.INCOMING_CALL"; + public static final String ACTION_CANCEL_CALL = "com.hoxfon.react.TwilioVoice.CANCEL_CALL"; public static final String ACTION_FCM_TOKEN = "com.hoxfon.react.TwilioVoice.ACTION_FCM_TOKEN"; public static final String ACTION_MISSED_CALL = "com.hoxfon.react.TwilioVoice.MISSED_CALL"; public static final String ACTION_ANSWER_CALL = "com.hoxfon.react.TwilioVoice.ANSWER_CALL"; @@ -179,7 +182,7 @@ public void onHostPause() { public void onHostDestroy() { disconnect(); callNotificationManager.removeHangupNotification(getReactApplicationContext()); - unsetAudioFocus(); + setAudioFocus(false); } @Override @@ -217,12 +220,30 @@ public void onError(RegistrationException error, String accessToken, String fcmT private Call.Listener callListener() { return new Call.Listener() { + /* + * This callback is emitted once before the Call.Listener.onConnected() callback when + * the callee is being alerted of a Call. The behavior of this callback is determined by + * the answerOnBridge flag provided in the Dial verb of your TwiML application + * associated with this client. If the answerOnBridge flag is false, which is the + * default, the Call.Listener.onConnected() callback will be emitted immediately after + * Call.Listener.onRinging(). If the answerOnBridge flag is true, this will cause the + * call to emit the onConnected callback only after the call is answered. + * See answeronbridge for more details on how to use it with the Dial TwiML verb. If the + * twiML response contains a Say verb, then the call will emit the + * Call.Listener.onConnected callback immediately after Call.Listener.onRinging() is + * raised, irrespective of the value of answerOnBridge being set to true or false + */ + @Override + public void onRinging(Call call) { + Log.d(TAG, "Ringing"); + } + @Override public void onConnected(Call call) { if (BuildConfig.DEBUG) { Log.d(TAG, "CALL CONNECTED callListener().onConnected call state = "+call.getState()); } - setAudioFocus(); + setAudioFocus(true); proximityManager.startProximitySensor(); headsetManager.startWiredHeadsetEvent(getReactApplicationContext()); @@ -244,10 +265,20 @@ public void onConnected(Call call) { } eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params); } + + @Override + public void onReconnecting(Call call, CallException callException) { + Log.d(TAG, "onReconnecting"); + } + + @Override + public void onReconnected(Call call) { + Log.d(TAG, "onReconnected"); + } @Override public void onDisconnected(Call call, CallException error) { - unsetAudioFocus(); + setAudioFocus(false); proximityManager.stopProximitySensor(); headsetManager.stopWiredHeadsetEvent(getReactApplicationContext()); callAccepted = false; @@ -281,7 +312,7 @@ public void onDisconnected(Call call, CallException error) { @Override public void onConnectFailure(Call call, CallException error) { - unsetAudioFocus(); + setAudioFocus(false); proximityManager.stopProximitySensor(); callAccepted = false; if (BuildConfig.DEBUG) { @@ -389,7 +420,7 @@ private void handleIncomingCallIntent(Intent intent) { if (intent.getAction().equals(ACTION_INCOMING_CALL)) { activeCallInvite = intent.getParcelableExtra(INCOMING_CALL_INVITE); - if (activeCallInvite != null && (activeCallInvite.getState() == CallInvite.State.PENDING)) { + if (activeCallInvite != null) { /* && (activeCallInvite.getState() == CallInvite.State.PENDING)) {*/ callAccepted = false; if (BuildConfig.DEBUG) { Log.d(TAG, "handleIncomingCallIntent state = PENDING"); @@ -412,57 +443,45 @@ private void handleIncomingCallIntent(Intent intent) { params.putString("call_sid", activeCallInvite.getCallSid()); params.putString("call_from", activeCallInvite.getFrom()); params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); + params.putString("call_state", "PENDING"); eventManager.sendEvent(EVENT_DEVICE_DID_RECEIVE_INCOMING, params); } - - } else { - if (BuildConfig.DEBUG) { - Log.d(TAG, "====> BEGIN handleIncomingCallIntent when activeCallInvite != PENDING"); - } - // this block is executed when the callInvite is cancelled and: - // - the call is answered (activeCall != null) - // - the call is rejected - SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); + } + } else if (intent.getAction().equals(ACTION_CANCEL_CALL)) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "====> BEGIN handleIncomingCallIntent when activeCallInvite != PENDING"); + } + // this block is executed when the callInvite is cancelled and: + // - the call is answered (activeCall != null) + // - the call is rejected - // the call is not active yet - if (activeCall == null) { + SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); - if (activeCallInvite != null) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "activeCallInvite state = " + activeCallInvite.getState()); - } - if (BuildConfig.DEBUG) { - Log.d(TAG, "activeCallInvite was cancelled by " + activeCallInvite.getFrom()); - } - if (!callAccepted) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "creating a missed call, activeCallInvite state: " + activeCallInvite.getState()); - } - callNotificationManager.createMissedCallNotification(getReactApplicationContext(), activeCallInvite); - int appImportance = callNotificationManager.getApplicationImportance(getReactApplicationContext()); - if (appImportance != RunningAppProcessInfo.IMPORTANCE_BACKGROUND) { - WritableMap params = Arguments.createMap(); - params.putString("call_sid", activeCallInvite.getCallSid()); - params.putString("call_from", activeCallInvite.getFrom()); - params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); - eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); - } - } - } - clearIncomingNotification(activeCallInvite); - } else { - if (BuildConfig.DEBUG) { - Log.d(TAG, "activeCallInvite was answered. Call " + activeCall); - } - } + if (BuildConfig.DEBUG) { + // Log.d(TAG, "activeCallInvite state = " + activeCallInvite.getState()); + } + if (BuildConfig.DEBUG) { + Log.d(TAG, "activeCallInvite was cancelled by " + activeCallInvite.getFrom()); + } + if (!callAccepted) { if (BuildConfig.DEBUG) { - Log.d(TAG, "====> END"); + Log.d(TAG, "creating a missed call"); + } + callNotificationManager.createMissedCallNotification(getReactApplicationContext(), activeCallInvite); + int appImportance = callNotificationManager.getApplicationImportance(getReactApplicationContext()); + if (appImportance != RunningAppProcessInfo.IMPORTANCE_BACKGROUND) { + WritableMap params = Arguments.createMap(); + params.putString("call_sid", activeCallInvite.getCallSid()); + params.putString("call_from", activeCallInvite.getFrom()); + params.putString("call_to", activeCallInvite.getTo()); + params.putString("call_state", "DISCONNECTED"); + eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); } } + + clearIncomingNotification(activeCallInvite); } else if (intent.getAction().equals(ACTION_FCM_TOKEN)) { if (BuildConfig.DEBUG) { Log.d(TAG, "handleIncomingCallIntent ACTION_FCM_TOKEN"); @@ -478,7 +497,7 @@ public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (action.equals(ACTION_INCOMING_CALL)) { if (BuildConfig.DEBUG) { - Log.d(TAG, "VoiceBroadcastReceiver.onReceive ACTION_INCOMING_CALL. Intent "+ intent.getExtras()); + Log.d(TAG, "VoiceBroadcastReceiver.onReceive ACTION_INCOMING_CALL. Intent " + intent.getExtras()); } handleIncomingCallIntent(intent); } else if (action.equals(ACTION_MISSED_CALL)) { @@ -516,7 +535,7 @@ public void initWithAccessToken(final String accessToken, Promise promise) { private void clearIncomingNotification(CallInvite callInvite) { if (BuildConfig.DEBUG) { - Log.d(TAG, "clearIncomingNotification() callInvite state: "+ callInvite.getState()); + // Log.d(TAG, "clearIncomingNotification() callInvite state: "+ callInvite.getState()); } if (callInvite != null && callInvite.getCallSid() != null) { // remove incoming call notification @@ -554,7 +573,7 @@ public void onComplete(@NonNull Task task) { if (BuildConfig.DEBUG) { Log.d(TAG, "Registering with FCM"); } - Voice.register(getReactApplicationContext(), accessToken, Voice.RegistrationChannel.FCM, fcmToken, registrationListener); + Voice.register(accessToken, Voice.RegistrationChannel.FCM, fcmToken, registrationListener); } } }); @@ -566,26 +585,26 @@ public void accept() { callAccepted = true; SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); if (activeCallInvite != null){ - if (activeCallInvite.getState() == CallInvite.State.PENDING) { + // if (activeCallInvite.getState() == CallInvite.State.PENDING) { if (BuildConfig.DEBUG) { Log.d(TAG, "accept() activeCallInvite.getState() PENDING"); } activeCallInvite.accept(getReactApplicationContext(), callListener); clearIncomingNotification(activeCallInvite); - } else { + // } else { // when the user answers a call from a notification before the react-native App // is completely initialised, and the first event has been skipped // re-send connectionDidConnect message to JS - WritableMap params = Arguments.createMap(); - params.putString("call_sid", activeCallInvite.getCallSid()); - params.putString("call_from", activeCallInvite.getFrom()); - params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); - callNotificationManager.createHangupLocalNotification(getReactApplicationContext(), - activeCallInvite.getCallSid(), - activeCallInvite.getFrom()); - eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params); - } + // WritableMap params = Arguments.createMap(); + // params.putString("call_sid", activeCallInvite.getCallSid()); + // params.putString("call_from", activeCallInvite.getFrom()); + // params.putString("call_to", activeCallInvite.getTo()); + // params.putString("call_state", activeCallInvite.getState().name()); + // callNotificationManager.createHangupLocalNotification(getReactApplicationContext(), + // activeCallInvite.getCallSid(), + // activeCallInvite.getFrom()); + // eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params); + // } } else { eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, null); } @@ -600,7 +619,7 @@ public void reject() { params.putString("call_sid", activeCallInvite.getCallSid()); params.putString("call_from", activeCallInvite.getFrom()); params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); + params.putString("call_state", "REJECTED"); activeCallInvite.reject(getReactApplicationContext()); clearIncomingNotification(activeCallInvite); } @@ -616,7 +635,7 @@ public void ignore() { params.putString("call_sid", activeCallInvite.getCallSid()); params.putString("call_from", activeCallInvite.getFrom()); params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); + params.putString("call_state", "BUSY"); clearIncomingNotification(activeCallInvite); } eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); @@ -673,7 +692,11 @@ public void connect(ReadableMap params) { } } - activeCall = Voice.call(getReactApplicationContext(), accessToken, twiMLParams, callListener); + ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken) + .params(twiMLParams) + .build(); + + activeCall = Voice.connect(getReactApplicationContext(), connectOptions, callListener); } @ReactMethod @@ -714,13 +737,15 @@ public void getActiveCall(Promise promise) { } if (activeCallInvite != null) { if (BuildConfig.DEBUG) { - Log.d(TAG, "Active call invite found state = "+activeCallInvite.getState()); + // Log.d(TAG, "Active call invite found state = "+activeCallInvite.getState()); } WritableMap params = Arguments.createMap(); params.putString("call_sid", activeCallInvite.getCallSid()); params.putString("call_from", activeCallInvite.getFrom()); params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); + if (activeCall != null) { + params.putString("call_state", activeCall.getState().name()); + } promise.resolve(params); return; } @@ -734,53 +759,46 @@ public void setSpeakerPhone(Boolean value) { audioManager.setSpeakerphoneOn(value); } - private void setAudioFocus() { - if (audioManager == null) { - return; - } - originalAudioMode = audioManager.getMode(); - // Request audio focus before making any device switch - if (Build.VERSION.SDK_INT >= 26) { - AudioAttributes playbackAttributes = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .build(); - focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) - .setAudioAttributes(playbackAttributes) - .setAcceptsDelayedFocusGain(true) - .setOnAudioFocusChangeListener(new AudioManager.OnAudioFocusChangeListener() { - @Override - public void onAudioFocusChange(int i) { } - }) - .build(); - audioManager.requestAudioFocus(focusRequest); - } else { - audioManager.requestAudioFocus( - null, - AudioManager.STREAM_VOICE_CALL, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE - ); - } - /* - * Start by setting MODE_IN_COMMUNICATION as default audio mode. It is - * required to be in this mode when playout and/or recording starts for - * best possible VoIP performance. Some devices have difficulties with speaker mode - * if this is not set. - */ - audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); - } + private void setAudioFocus(boolean setFocus) { + if (audioManager != null) { + if (setFocus) { + savedAudioMode = audioManager.getMode(); + // Request audio focus before making any device switch. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + AudioAttributes playbackAttributes = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .build(); + AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) + .setAudioAttributes(playbackAttributes) + .setAcceptsDelayedFocusGain(true) + .setOnAudioFocusChangeListener(new AudioManager.OnAudioFocusChangeListener() { + @Override + public void onAudioFocusChange(int i) { + } + }) + .build(); + audioManager.requestAudioFocus(focusRequest); + } else { + int focusRequestResult = audioManager.requestAudioFocus(new AudioManager.OnAudioFocusChangeListener() { - private void unsetAudioFocus() { - if (audioManager == null) { - return; - } - audioManager.setMode(originalAudioMode); - if (Build.VERSION.SDK_INT >= 26) { - if (focusRequest != null) { - audioManager.abandonAudioFocusRequest(focusRequest); + @Override + public void onAudioFocusChange(int focusChange) { + } + }, AudioManager.STREAM_VOICE_CALL, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + } + /* + * Start by setting MODE_IN_COMMUNICATION as default audio mode. It is + * required to be in this mode when playout and/or recording starts for + * best possible VoIP performance. Some devices have difficulties with speaker mode + * if this is not set. + */ + audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); + } else { + audioManager.setMode(savedAudioMode); + audioManager.abandonAudioFocus(null); } - } else { - audioManager.abandonAudioFocus(null); } } diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java index ca5d4fed..f1841f0e 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java @@ -4,6 +4,7 @@ import android.app.ActivityManager; import android.content.Intent; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import androidx.localbroadcastmanager.content.LocalBroadcastManager; @@ -18,8 +19,9 @@ import com.google.firebase.messaging.RemoteMessage; import com.hoxfon.react.RNTwilioVoice.BuildConfig; import com.hoxfon.react.RNTwilioVoice.CallNotificationManager; +import com.twilio.voice.CallException; import com.twilio.voice.CallInvite; -import com.twilio.voice.MessageException; +import com.twilio.voice.CancelledCallInvite; import com.twilio.voice.MessageListener; import com.twilio.voice.Voice; @@ -29,7 +31,9 @@ import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_FCM_TOKEN; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_INCOMING_CALL; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_CANCEL_CALL; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_INVITE; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.CANCELLED_CALL_INVITE; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_NOTIFICATION_ID; import com.hoxfon.react.RNTwilioVoice.SoundPoolManager; @@ -60,7 +64,9 @@ public void onNewToken(String token) { @Override public void onMessageReceived(RemoteMessage remoteMessage) { if (BuildConfig.DEBUG) { + Log.d(TAG, "Received onMessageReceived()"); Log.d(TAG, "Bundle data: " + remoteMessage.getData()); + Log.d(TAG, "From: " + remoteMessage.getFrom()); } // Check if message contains a data payload. @@ -71,8 +77,7 @@ public void onMessageReceived(RemoteMessage remoteMessage) { Random randomNumberGenerator = new Random(System.currentTimeMillis()); final int notificationId = randomNumberGenerator.nextInt(); - Voice.handleMessage(this, data, new MessageListener() { - + boolean valid = Voice.handleMessage(getApplicationContext(), data, new MessageListener() { @Override public void onCallInvite(final CallInvite callInvite) { @@ -126,10 +131,23 @@ public void onReactContextInitialized(ReactContext context) { } @Override - public void onError(MessageException messageException) { - Log.e(TAG, "Error handling FCM message" + messageException.toString()); + public void onCancelledCallInvite(final CancelledCallInvite cancelledCallInvite, final CallException callException) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + public void run() { + ReactInstanceManager mReactInstanceManager = ((ReactApplication) getApplication()).getReactNativeHost().getReactInstanceManager(); + ReactContext context = mReactInstanceManager.getCurrentReactContext(); + VoiceFirebaseMessagingService.this.cancelNotification((ReactApplicationContext)context, cancelledCallInvite); + VoiceFirebaseMessagingService.this.sendCancelledCallInviteToActivity( + cancelledCallInvite); + } + }); } }); + + if (!valid) { + Log.e(TAG, "Error handling FCM message"); + } } // Check if message contains a notification payload. @@ -161,6 +179,15 @@ private void sendIncomingCallMessageToActivity( LocalBroadcastManager.getInstance(context).sendBroadcast(intent); } + /* + * Send the CancelledCallInvite to the VoiceActivity + */ + private void sendCancelledCallInviteToActivity(CancelledCallInvite cancelledCallInvite) { + Intent intent = new Intent(ACTION_CANCEL_CALL); + intent.putExtra(CANCELLED_CALL_INVITE, cancelledCallInvite); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + /* * Show the notification in the Android notification drawer */ @@ -170,11 +197,12 @@ private void showNotification(ReactApplicationContext context, int notificationId, Intent launchIntent ) { - if (callInvite != null && callInvite.getState() == CallInvite.State.PENDING) { - callNotificationManager.createIncomingCallNotification(context, callInvite, notificationId, launchIntent); - } else { - SoundPoolManager.getInstance(context.getBaseContext()).stopRinging(); - callNotificationManager.removeIncomingCallNotification(context, callInvite, 0); - } + callNotificationManager.createIncomingCallNotification(context, callInvite, notificationId, launchIntent); } + + private void cancelNotification(ReactApplicationContext context, CancelledCallInvite cancelledCallInvite) { + SoundPoolManager.getInstance((this)).stopRinging(); + callNotificationManager.removeIncomingCallNotification(context, cancelledCallInvite, 0); + } + } diff --git a/ios/RNTwilioVoice/RNTwilioVoice.m b/ios/RNTwilioVoice/RNTwilioVoice.m index 7fc54618..b7ced30b 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.m +++ b/ios/RNTwilioVoice/RNTwilioVoice.m @@ -16,9 +16,14 @@ @interface RNTwilioVoice () *)supportedEvents { - return @[@"connectionDidConnect", @"connectionDidDisconnect", @"callRejected", @"deviceReady", @"deviceNotReady"]; + return @[@"connectionDidConnect", @"connectionDidDisconnect", @"callRejected", @"deviceReady", @"deviceNotReady", @"deviceDidReceiveIncoming"]; } @synthesize bridge = _bridge; @@ -70,6 +75,7 @@ - (void)dealloc { RCT_EXPORT_METHOD(configureCallKit: (NSDictionary *)params) { if (self.callKitCallController == nil) { + [self initRNTwilioVoice]; _settings = [[NSMutableDictionary alloc] initWithDictionary:params]; CXProviderConfiguration *configuration = [[CXProviderConfiguration alloc] initWithLocalizedName:params[@"appName"]]; configuration.maximumCallGroups = 1; @@ -93,8 +99,6 @@ - (void)dealloc { RCT_EXPORT_METHOD(connect: (NSDictionary *)params) { NSLog(@"Calling phone number %@", [params valueForKey:@"To"]); -// [TwilioVoice setLogLevel:TVOLogLevelVerbose]; - UIDevice* device = [UIDevice currentDevice]; device.proximityMonitoringEnabled = YES; @@ -110,16 +114,17 @@ - (void)dealloc { RCT_EXPORT_METHOD(disconnect) { NSLog(@"Disconnecting call"); + self.userInitiatedDisconnect = YES; [self performEndCallActionWithUUID:self.call.uuid]; } RCT_EXPORT_METHOD(setMuted: (BOOL *)muted) { NSLog(@"Mute/UnMute call"); - self.call.muted = muted; + self.call.muted = muted ? YES : NO; } RCT_EXPORT_METHOD(setSpeakerPhone: (BOOL *)speaker) { - [self toggleAudioRoute:speaker]; + [self toggleAudioRoute:speaker ? YES : NO]; } RCT_EXPORT_METHOD(sendDigits: (NSString *)digits) { @@ -150,23 +155,18 @@ - (void)dealloc { resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) { NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - if (self.callInvite) { - if (self.callInvite.callSid) { - [params setObject:self.callInvite.callSid forKey:@"call_sid"]; - } - if (self.callInvite.from) { - [params setObject:self.callInvite.from forKey:@"from"]; + if (self.activeCallInvites.count) { + TVOCallInvite *callInvite = [self.activeCallInvites valueForKey:[self.activeCallInvites allKeys][self.activeCallInvites.count-1]]; + if (callInvite.callSid) { + [params setObject:callInvite.callSid forKey:@"call_sid"]; } - if (self.callInvite.to) { - [params setObject:self.callInvite.to forKey:@"to"]; + if (callInvite.from) { + [params setObject:callInvite.from forKey:@"call_from"]; } - if (self.callInvite.state == TVOCallInviteStatePending) { - [params setObject:StatePending forKey:@"call_state"]; - } else if (self.callInvite.state == TVOCallInviteStateCanceled) { - [params setObject:StateDisconnected forKey:@"call_state"]; - } else if (self.callInvite.state == TVOCallInviteStateRejected) { - [params setObject:StateRejected forKey:@"call_state"]; + if (callInvite.to) { + [params setObject:callInvite.to forKey:@"call_to"]; } + [params setObject:StatePending forKey:@"call_state"]; resolve(params); } else if (self.call) { if (self.call.sid) { @@ -191,6 +191,19 @@ - (void)dealloc { } } +- (void)initRNTwilioVoice { + /* + * The important thing to remember when providing a TVOAudioDevice is that the device must be set + * before performing any other actions with the SDK (such as connecting a Call, or accepting an incoming Call). + * In this case we've already initialized our own `TVODefaultAudioDevice` instance which we will now set. + */ + self.audioDevice = [TVODefaultAudioDevice audioDevice]; + TwilioVoice.audioDevice = self.audioDevice; + + self.activeCallInvites = [NSMutableDictionary dictionary]; + self.activeCalls = [NSMutableDictionary dictionary]; + } + - (void)initPushRegistry { self.voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()]; self.voipRegistry.delegate = self; @@ -258,65 +271,113 @@ - (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(P } } -- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type { - NSLog(@"pushRegistry:didReceiveIncomingPushWithPayload:forType"); +- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion { + NSLog(@"pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:"); if ([type isEqualToString:PKPushTypeVoIP]) { - [TwilioVoice handleNotification:payload.dictionaryPayload - delegate:self]; - } -} - -#pragma mark - TVONotificationDelegate -- (void)callInviteReceived:(TVOCallInvite *)callInvite { - if (callInvite.state == TVOCallInviteStatePending) { - [self handleCallInviteReceived:callInvite]; - } else if (callInvite.state == TVOCallInviteStateCanceled) { - [self handleCallInviteCanceled:callInvite]; + // The Voice SDK will use main queue to invoke `cancelledCallInviteReceived:error` when delegate queue is not passed + if (![TwilioVoice handleNotification:payload.dictionaryPayload delegate:self delegateQueue:nil]) { + NSLog(@"This is not a valid Twilio Voice notification."); } -} - -- (void)handleCallInviteReceived:(TVOCallInvite *)callInvite { - NSLog(@"callInviteReceived:"); - if (self.callInvite && self.callInvite == TVOCallInviteStatePending) { - NSLog(@"Already a pending incoming call invite."); - NSLog(@" >> Ignoring call from %@", callInvite.from); - return; - } else if (self.call) { - NSLog(@"Already an active call."); - NSLog(@" >> Ignoring call from %@", callInvite.from); - return; } - - self.callInvite = callInvite; - - [self reportIncomingCallFrom:callInvite.from withUUID:callInvite.uuid]; -} - -- (void)handleCallInviteCanceled:(TVOCallInvite *)callInvite { - NSLog(@"callInviteCanceled"); - - [self performEndCallActionWithUUID:callInvite.uuid]; - - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - if (self.callInvite.callSid) { - [params setObject:self.callInvite.callSid forKey:@"call_sid"]; + if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) { + // Save for later when the notification is properly handled. + self.incomingPushCompletionCallback = completion; + } else { + /** + * The Voice SDK processes the call notification and returns the call invite synchronously. Report the incoming call to + * CallKit and fulfill the completion before exiting this callback method. + */ + completion(); } +} - if (self.callInvite.from) { - [params setObject:self.callInvite.from forKey:@"call_from"]; - } - if (self.callInvite.to) { - [params setObject:self.callInvite.to forKey:@"call_to"]; - } - if (self.callInvite.state == TVOCallInviteStateCanceled) { - [params setObject:StateDisconnected forKey:@"call_state"]; - } else if (self.callInvite.state == TVOCallInviteStateRejected) { - [params setObject:StateRejected forKey:@"call_state"]; - } - [self sendEventWithName:@"connectionDidDisconnect" body:params]; +- (void)incomingPushHandled { + if (self.incomingPushCompletionCallback) { + self.incomingPushCompletionCallback(); + self.incomingPushCompletionCallback = nil; + } +} - self.callInvite = nil; +#pragma mark - TVONotificationDelegate +- (void)callInviteReceived:(TVOCallInvite *)callInvite { + /** + * Calling `[TwilioVoice handleNotification:delegate:]` will synchronously process your notification payload and + * provide you a `TVOCallInvite` object. Report the incoming call to CallKit upon receiving this callback. + */ + + NSLog(@"callInviteReceived:"); + + NSString *from = @"Unknown"; + if (callInvite.from) { + from = [callInvite.from stringByReplacingOccurrencesOfString:@"client:" withString:@""]; + } + + // Always report to CallKit + [self reportIncomingCallFrom:from withUUID:callInvite.uuid]; + self.activeCallInvites[[callInvite.uuid UUIDString]] = callInvite; + if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) { + [self incomingPushHandled]; + } + + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (callInvite.callSid) { + [params setObject:callInvite.callSid forKey:@"call_sid"]; + } + + if (callInvite.from) { + [params setObject:callInvite.from forKey:@"call_from"]; + } + if (callInvite.to) { + [params setObject:callInvite.to forKey:@"call_to"]; + } + + [params setObject:StatePending forKey:@"call_state"]; + + [self sendEventWithName:@"deviceDidReceiveIncoming" body:params]; +} + +- (void)cancelledCallInviteReceived:(TVOCancelledCallInvite *)cancelledCallInvite error:(NSError *)error { + + /** + * The SDK may call `[TVONotificationDelegate callInviteReceived:error:]` asynchronously on the dispatch queue + * with a `TVOCancelledCallInvite` if the caller hangs up or the client encounters any other error before the called + * party could answer or reject the call. + */ + + NSLog(@"cancelledCallInviteReceived:"); + + TVOCallInvite *callInvite; + for (NSString *activeCallInviteId in self.activeCallInvites) { + TVOCallInvite *activeCallInvite = [self.activeCallInvites objectForKey:activeCallInviteId]; + if ([cancelledCallInvite.callSid isEqualToString:activeCallInvite.callSid]) { + callInvite = activeCallInvite; + break; + } + } + + if (callInvite) { + [self performEndCallActionWithUUID:callInvite.uuid]; + + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (callInvite.callSid) { + [params setObject:callInvite.callSid forKey:@"call_sid"]; + } + if (callInvite.from) { + [params setObject:callInvite.from forKey:@"call_from"]; + } + if (callInvite.to) { + [params setObject:callInvite.to forKey:@"call_to"]; + } + + if (error.code == TVOErrorCallCancelledError) { + [params setObject:StateDisconnected forKey:@"call_state"]; + } else { + [params setObject:StateRejected forKey:@"call_state"]; + } + + [self sendEventWithName:@"connectionDidDisconnect" body:params]; + } } - (void)notificationError:(NSError *)error { @@ -325,9 +386,8 @@ - (void)notificationError:(NSError *)error { #pragma mark - TVOCallDelegate - (void)callDidConnect:(TVOCall *)call { - self.call = call; + NSLog(@"callDidConnect:"); self.callKitCompletionCallback(YES); - self.callKitCompletionCallback = nil; NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; [callParams setObject:call.sid forKey:@"call_sid"]; @@ -351,66 +411,87 @@ - (void)call:(TVOCall *)call didFailToConnectWithError:(NSError *)error { self.callKitCompletionCallback(NO); [self performEndCallActionWithUUID:call.uuid]; - [self callDisconnected:error]; + [self call:call disconnectedWithError:error]; } - (void)call:(TVOCall *)call didDisconnectWithError:(NSError *)error { - NSLog(@"Call disconnected with error: %@", error); - - [self callDisconnected:error]; -} - -- (void)callDisconnected:(NSError *)error { - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; if (error) { - NSString* errMsg = [error localizedDescription]; - if (error.localizedFailureReason) { - errMsg = [error localizedFailureReason]; - } - [params setObject:errMsg forKey:@"err"]; - } - if (self.call.sid) { - [params setObject:self.call.sid forKey:@"call_sid"]; - } - if (self.call.to) { - [params setObject:self.call.to forKey:@"call_to"]; - } - if (self.call.from) { - [params setObject:self.call.from forKey:@"call_from"]; + NSLog(@"Call failed: %@", error); + } else { + NSLog(@"Call disconnected"); } - if (self.call.state == TVOCallStateDisconnected) { - [params setObject:StateDisconnected forKey:@"call_state"]; + + if (!self.userInitiatedDisconnect) { + CXCallEndedReason reason = CXCallEndedReasonRemoteEnded; + if (error) { + reason = CXCallEndedReasonFailed; + } + + [self.callKitProvider reportCallWithUUID:call.uuid endedAtDate:[NSDate date] reason:reason]; } - [self sendEventWithName:@"connectionDidDisconnect" body:params]; - self.call = nil; - self.callKitCompletionCallback = nil; -} + [self call:call disconnectedWithError:error]; +} + +- (void)call:(TVOCall *)call disconnectedWithError:(NSError *)error { + if ([call isEqual:self.call]) { + self.call = nil; + } + [self.activeCalls removeObjectForKey:call.uuid.UUIDString]; + + self.userInitiatedDisconnect = NO; + + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (error) { + NSString* errMsg = [error localizedDescription]; + if (error.localizedFailureReason) { + errMsg = [error localizedFailureReason]; + } + [params setObject:errMsg forKey:@"err"]; + } + if (call.sid) { + [params setObject:call.sid forKey:@"call_sid"]; + } + if (call.to) { + [params setObject:call.to forKey:@"call_to"]; + } + if (call.from) { + [params setObject:call.from forKey:@"call_from"]; + } + if (call.state == TVOCallStateDisconnected) { + [params setObject:StateDisconnected forKey:@"call_state"]; + } + [self sendEventWithName:@"connectionDidDisconnect" body:params]; + } #pragma mark - AVAudioSession -- (void)toggleAudioRoute: (BOOL *)toSpeaker { +- (void)toggleAudioRoute: (BOOL)toSpeaker { // The mode set by the Voice SDK is "VoiceChat" so the default audio route is the built-in receiver. // Use port override to switch the route. - NSError *error = nil; - NSLog(@"toggleAudioRoute"); - - if (toSpeaker) { - if (![[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker - error:&error]) { - NSLog(@"Unable to reroute audio: %@", [error localizedDescription]); - } - } else { - if (![[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone - error:&error]) { - NSLog(@"Unable to reroute audio: %@", [error localizedDescription]); - } - } + self.audioDevice.block = ^ { + // We will execute `kDefaultAVAudioSessionConfigurationBlock` first. + kTVODefaultAVAudioSessionConfigurationBlock(); + + // Overwrite the audio route + AVAudioSession *session = [AVAudioSession sharedInstance]; + NSError *error = nil; + if (toSpeaker) { + if (![session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error]) { + NSLog(@"Unable to reroute audio: %@", [error localizedDescription]); + } + } else { + if (![session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&error]) { + NSLog(@"Unable to reroute audio: %@", [error localizedDescription]); + } + } + }; + self.audioDevice.block(); } #pragma mark - CXProviderDelegate - (void)providerDidReset:(CXProvider *)provider { NSLog(@"providerDidReset"); - TwilioVoice.audioEnabled = YES; + self.audioDevice.enabled = YES; } - (void)providerDidBegin:(CXProvider *)provider { @@ -419,12 +500,11 @@ - (void)providerDidBegin:(CXProvider *)provider { - (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession { NSLog(@"provider:didActivateAudioSession"); - TwilioVoice.audioEnabled = YES; + self.audioDevice.enabled = YES; } - (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession { NSLog(@"provider:didDeactivateAudioSession"); - TwilioVoice.audioEnabled = NO; } - (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action { @@ -434,8 +514,8 @@ - (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)act - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action { NSLog(@"provider:performStartCallAction"); - [TwilioVoice configureAudioSession]; - TwilioVoice.audioEnabled = NO; + self.audioDevice.enabled = NO; + self.audioDevice.block(); [self.callKitProvider reportOutgoingCallWithUUID:action.callUUID startedConnectingAtDate:[NSDate date]]; @@ -454,14 +534,9 @@ - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallActio - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action { NSLog(@"provider:performAnswerCallAction"); - // RCP: Workaround from https://forums.developer.apple.com/message/169511 suggests configuring audio in the - // completion block of the `reportNewIncomingCallWithUUID:update:completion:` method instead of in - // `provider:performAnswerCallAction:` per the WWDC examples. - // [TwilioVoice configureAudioSession]; - - NSAssert([self.callInvite.uuid isEqual:action.callUUID], @"We only support one Invite at a time."); + self.audioDevice.enabled = NO; + self.audioDevice.block(); - TwilioVoice.audioEnabled = NO; [self performAnswerVoiceCallWithUUID:action.callUUID completion:^(BOOL success) { if (success) { [action fulfill]; @@ -476,28 +551,43 @@ - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAct - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action { NSLog(@"provider:performEndCallAction"); - TwilioVoice.audioEnabled = NO; + TVOCallInvite *callInvite = self.activeCallInvites[action.callUUID.UUIDString]; + TVOCall *call = self.activeCalls[action.callUUID.UUIDString]; - if (self.callInvite && self.callInvite.state == TVOCallInviteStatePending) { + if (callInvite) { [self sendEventWithName:@"callRejected" body:@"callRejected"]; - [self.callInvite reject]; - self.callInvite = nil; - } else if (self.call) { - [self.call disconnect]; + [callInvite reject]; + [self.activeCallInvites removeObjectForKey:callInvite.uuid.UUIDString]; + } else if (call) { + [call disconnect]; + } else { + NSLog(@"Unknown UUID to perform end-call action with"); } + self.audioDevice.enabled = YES; [action fulfill]; } - (void)provider:(CXProvider *)provider performSetHeldCallAction:(CXSetHeldCallAction *)action { - if (self.call && self.call.state == TVOCallStateConnected) { - [self.call setOnHold:action.isOnHold]; + TVOCall *call = self.activeCalls[action.callUUID.UUIDString]; + if (call) { + [call setOnHold:action.isOnHold]; [action fulfill]; } else { [action fail]; } } +- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action { + TVOCall *call = self.activeCalls[action.callUUID.UUIDString]; + if (call) { + [call setMuted:action.isMuted]; + [action fulfill]; + } else { + [action fail]; + } +} + #pragma mark - CallKit Actions - (void)performStartCallActionWithUUID:(NSUUID *)uuid handle:(NSString *)handle { if (uuid == nil || handle == nil) { @@ -541,9 +631,6 @@ - (void)reportIncomingCallFrom:(NSString *)from withUUID:(NSUUID *)uuid { [self.callKitProvider reportNewIncomingCallWithUUID:uuid update:callUpdate completion:^(NSError *error) { if (!error) { NSLog(@"Incoming call successfully reported"); - - // RCP: Workaround per https://forums.developer.apple.com/message/169511 - [TwilioVoice configureAudioSession]; } else { NSLog(@"Failed to report incoming call successfully: %@.", [error localizedDescription]); } @@ -574,20 +661,43 @@ - (void)performVoiceCallWithUUID:(NSUUID *)uuid client:(NSString *)client completion:(void(^)(BOOL success))completionHandler { - self.call = [TwilioVoice call:[self fetchAccessToken] - params:_callParams - uuid:uuid - delegate:self]; - + __weak typeof(self) weakSelf = self; + TVOConnectOptions *connectOptions = [TVOConnectOptions optionsWithAccessToken:[self fetchAccessToken] block:^(TVOConnectOptionsBuilder *builder) { + __strong typeof(self) strongSelf = weakSelf; + builder.params = strongSelf->_callParams; + builder.uuid = uuid; + }]; + TVOCall *call = [TwilioVoice connectWithOptions:connectOptions delegate:self]; + if (call) { + self.call = call; + self.activeCalls[call.uuid.UUIDString] = call; + } self.callKitCompletionCallback = completionHandler; } - (void)performAnswerVoiceCallWithUUID:(NSUUID *)uuid completion:(void(^)(BOOL success))completionHandler { + TVOCallInvite *callInvite = self.activeCallInvites[uuid.UUIDString]; + NSAssert(callInvite, @"No CallInvite matches the UUID"); + TVOAcceptOptions *acceptOptions = [TVOAcceptOptions optionsWithCallInvite:callInvite block:^(TVOAcceptOptionsBuilder *builder) { + builder.uuid = callInvite.uuid; + }]; - self.call = [self.callInvite acceptWithDelegate:self]; - self.callInvite = nil; - self.callKitCompletionCallback = completionHandler; + TVOCall *call = [callInvite acceptWithOptions:acceptOptions delegate:self]; + + if (!call) { + completionHandler(NO); + } else { + self.callKitCompletionCallback = completionHandler; + self.call = call; + self.activeCalls[call.uuid.UUIDString] = call; + } + + [self.activeCallInvites removeObjectForKey:callInvite.uuid.UUIDString]; + + if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) { + [self incomingPushHandled]; + } } - (void)handleAppTerminateNotification { From 85b594f1dce07efb99a3f49af60d723c71215d05 Mon Sep 17 00:00:00 2001 From: Craig Martin Date: Mon, 20 Jan 2020 09:26:31 -0500 Subject: [PATCH 14/14] Fix podspec path for TwilioVoice framework --- RNTwilioVoice.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RNTwilioVoice.podspec b/RNTwilioVoice.podspec index 9a9aac92..53f4ec2d 100644 --- a/RNTwilioVoice.podspec +++ b/RNTwilioVoice.podspec @@ -15,6 +15,6 @@ Pod::Spec.new do |s| s.source = {:path => "./RNTwilioVoice"} s.dependency 'React' - s.xcconfig = { 'FRAMEWORK_SEARCH_PATHS' => '${PODS_ROOT}/TwilioVoice' } + s.xcconfig = { 'FRAMEWORK_SEARCH_PATHS' => '${PODS_ROOT}/TwilioVoice/Build/iOS' } s.frameworks = 'TwilioVoice' end \ No newline at end of file