From 9c865c27fefbbe89eac061fd3bf89744fe8659c5 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sat, 15 Dec 2018 20:55:32 +0000 Subject: [PATCH 01/19] fix: iOS end-call flow avoids 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 42ff8f297bfff8e0e855d8bcfcc6690abe035644 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sat, 15 Dec 2018 20:57:49 +0000 Subject: [PATCH 02/19] 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 3fe2220c9300c71beae16820cbe2404d452623e1 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sat, 15 Dec 2018 21:00:39 +0000 Subject: [PATCH 03/19] 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 878fc179..09c2074c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -47,6 +47,6 @@ dependencies { compile 'com.twilio:voice-android:2.0.7' 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 67e814a61e9bf55ef9cebc5d16388fb33081cb47 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sat, 15 Dec 2018 20:58:14 +0000 Subject: [PATCH 04/19] update: implement Twilio Programmable Voice Android SDK 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 09c2074c..e2b65ac7 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.3.4' From 0a80e23e9d1f7dfbf5cd52537cec77e8ac4453d8 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sat, 15 Dec 2018 21:01:46 +0000 Subject: [PATCH 05/19] 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 c69155f32ddcf4a330ab0cca31e9e77e7252bdc4 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Thu, 27 Dec 2018 18:37:47 +0000 Subject: [PATCH 06/19] fix: align lib dependency with root update build.gradle as per recommendations in RN 0.57.8 update gradle to v 3.2.1 --- README.md | 2 +- android/build.gradle | 30 +++++++++++++++++++++--------- android/settings.gradle | 0 3 files changed, 22 insertions(+), 10 deletions(-) delete mode 100644 android/settings.gradle diff --git a/README.md b/README.md index ea7974c3..6ec1cdda 100644 --- a/README.md +++ b/README.md @@ -143,7 +143,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' } } 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 22f426791c4bcfc3e5d8044b0effcaf474905cf8 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Wed, 4 Dec 2019 21:07:35 +0000 Subject: [PATCH 07/19] 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 228b4b63..bd003952 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 9a77b70b2560cfa76992f4adab93f7898cd5fef5 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Wed, 4 Dec 2019 21:08:09 +0000 Subject: [PATCH 08/19] 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 08c29073cb1cf40f122acaf2c527042863cd9236 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Wed, 4 Dec 2019 21:08:58 +0000 Subject: [PATCH 09/19] 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 5d11092448fa898a20cd181c12e82641cacf2132 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Wed, 4 Dec 2019 21:09:17 +0000 Subject: [PATCH 10/19] 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 91fc2025374f3d9c3f558766f927b6e5bed10497 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sat, 15 Dec 2018 21:01:59 +0000 Subject: [PATCH 11/19] 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 6ec1cdda..d47f06ee 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 bd003952..64a2b54e 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 e7eff6e12d6878c5cdc033a48766ed0228b191aa Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Wed, 4 Dec 2019 21:31:29 +0000 Subject: [PATCH 12/19] 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 d47f06ee..31b49f5f 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 341aacbeb0fb13742dd9b77bce6e96095a947943 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sun, 12 Jan 2020 18:48:44 +0000 Subject: [PATCH 13/19] Android: implement Twilio Programmable Voice Android SDK 3.3.0 - new feature call hold - new feature call ringing state - new feature ConnectOptions - remove state for callInvite - handle internal CANCEL_CALL notification - remove blocks of code for callInvite PENDING --- CHANGELOG.md | 15 +- README.md | 76 +++-- android/build.gradle | 8 +- android/gradle.properties | 2 + .../CallNotificationManager.java | 13 +- .../react/RNTwilioVoice/EventManager.java | 3 + .../RNTwilioVoice/TwilioVoiceModule.java | 276 +++++++++++------- .../fcm/VoiceFirebaseMessagingService.java | 77 ++--- index.js | 29 +- 9 files changed, 294 insertions(+), 205 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3004414e..135314d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,14 +3,23 @@ ## 4.0.0 - Android + - implement new autolinking react native API - 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 + - upgrade com.twilio:voice-android to 3.3.0 + - implement `hold` to hold a call + - new event `callInviteCancelled` + - new event `callStateRinging` + - new method `getCallInvite` + - implement call ringing Twilio event + - remove `call_state` from CallInvite - 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` + +- throw an error when listening to events that do not exist ## 3.21.3 diff --git a/README.md b/README.md index 31b49f5f..88ff2846 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ This is a React Native wrapper for Twilio Programmable Voice SDK that lets you m # Twilio Programmable Voice SDK -- Android 2.1.0 (bundled within this library) +- Android 3.3.0 (bundled within this library) - iOS 2.1.0 (specified by the app's own podfile) ## Breaking changes in v4.0.0 @@ -21,12 +21,24 @@ This is a React Native wrapper for Twilio Programmable Voice SDK that lets you m ``` +Data passed to the event `deviceDidReceiveIncoming` does not contain the key `call_state`, because state of Call Invites was removed in Twilio Android v3.0.0 + - iOS: params changes for `connectionDidConnect` and `connectionDidDisconnect` to => call_to from => call_from error => err +New features + +Twilio Programmable Voice SDK v3.0.0 handles call invites directly and makes it easy to distinguish a call invites from an active call, which previously was confusing. +To ensure that an active call is displayed when the app comes to foreground you should use the promise `getActiveCall()`. +To ensure that a call invite is displayed when the app comes to foreground use the promise `getCallInvite()`. Please note that call invites don't have a `call_state` field. + +You should use `hold()` to put a call on hold. + +You can be notified when a call is `ringing` by listening for `callStateRinging` events. + ## Breaking changes in v3.0.0 - initWitToken returns an object with a property `initialized` instead of `initilized` @@ -251,44 +263,40 @@ TwilioVoice.addEventListener('deviceNotReady', function(data) { // } }) TwilioVoice.addEventListener('connectionDidConnect', function(data) { - // Android // { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', + // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // } - // iOS - // { - // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', - // from: string, // "+441234567890" // issue 44 (https://github.com/hoxfon/react-native-twilio-programmable-voice/issues/44) - // to: string, // "client:bob" // issue 44 (https://github.com/hoxfon/react-native-twilio-programmable-voice/issues/44) - // } }) TwilioVoice.addEventListener('connectionDidDisconnect', function(data: mixed) { // | null // | { // err: string // } - // | Android - // { + // | { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', + // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // err?: string, // } - // | iOS - // { - // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', - // call_from?: string, // "+441234567890" - // call_to?: string, // "client:bob" - // from?: string, // "+441234567890" // issue 44 (https://github.com/hoxfon/react-native-twilio-programmable-voice/issues/44) - // to?: string, // "client:bob" // issue 44 (https://github.com/hoxfon/react-native-twilio-programmable-voice/issues/44) - // error?: string, // issue 44 (https://github.com/hoxfon/react-native-twilio-programmable-voice/issues/44) - // } +}) +TwilioVoice.addEventListener('callStateRinging', function(data: mixed) { + // { + // call_sid: string, // Twilio call sid + // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', + // call_from: string, // "+441234567890" + // call_to: string, // "client:bob" + // } +}) +TwilioVoice.addEventListener('callInviteCancelled', function(data: mixed) { + // { + // call_sid: string, // Twilio call sid + // call_from: string, // "+441234567890" + // call_to: string, // "client:bob" + // } }) // iOS Only @@ -298,7 +306,6 @@ TwilioVoice.addEventListener('callRejected', function(value: 'callRejected') {}) TwilioVoice.addEventListener('deviceDidReceiveIncoming', function(data) { // { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // } @@ -339,14 +346,25 @@ TwilioVoice.ignore() // mutedValue must be a boolean TwilioVoice.setMuted(mutedValue) +// put a call on hold +TwilioVoice.setOnHold(holdValue) + +// send digits TwilioVoice.sendDigits(digits) -// should be called after the app is initialized -// to catch incoming call when the app was in the background +// Ensure that an active call is displayed when the app comes to foreground TwilioVoice.getActiveCall() - .then(incomingCall => { - if (incomingCall){ - _deviceDidReceiveIncoming(incomingCall) + .then(activeCall => { + if (activeCall){ + _displayActiveCall(activeCall) + } + }) + +// Ensure that call invites are displayed when the app comes to foreground +TwilioVoice.getCallInvite() + .then(callInvite => { + if (callInvite){ + _handleCallInvite(callInvite) } }) diff --git a/android/build.gradle b/android/build.gradle index b4365da1..a716af2c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,11 +26,15 @@ apply plugin: 'com.android.library' 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" +def DEFAULT_SUPPORT_LIB_VERSION = "28.0.3" android { compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION buildToolsVersion rootProject.hasProperty('buildToolsVersion') ? rootProject.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } defaultConfig { minSdkVersion 16 targetSdkVersion rootProject.hasProperty('targetSdkVersion') ? rootProject.targetSdkVersion : DEFAULT_TARGET_SDK_VERSION @@ -50,7 +54,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:3.3.0' 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..af4e9175 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,18 @@ public void createHangupLocalNotification(ReactApplicationContext context, Strin } public void removeIncomingCallNotification(ReactApplicationContext context, - CallInvite callInvite, + CancelledCallInvite callInvite, int notificationId) { - Log.d(TAG, "removeIncomingCallNotification"); + if (BuildConfig.DEBUG) { + Log.d(TAG, "removeIncomingCallNotification"); + } + if (context == null) { + Log.e(TAG, "Context is null"); + return; + } 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/EventManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java index af7a98aa..40b2fed7 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java @@ -21,6 +21,9 @@ public class EventManager { public static final String EVENT_CONNECTION_DID_CONNECT = "connectionDidConnect"; public static final String EVENT_CONNECTION_DID_DISCONNECT = "connectionDidDisconnect"; public static final String EVENT_DEVICE_DID_RECEIVE_INCOMING = "deviceDidReceiveIncoming"; + public static final String EVENT_CALL_STATE_RINGING = "callStateRinging"; + public static final String EVENT_CALL_INVITE_CANCELLED = "callInviteCancelled"; + public EventManager(ReactApplicationContext context) { mContext = context; 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..09573c77 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java @@ -43,9 +43,12 @@ import com.google.android.gms.tasks.Task; import com.google.firebase.iid.FirebaseInstanceId; import com.google.firebase.iid.InstanceIdResult; +import com.twilio.voice.AcceptOptions; import com.twilio.voice.Call; import com.twilio.voice.CallException; import com.twilio.voice.CallInvite; +import com.twilio.voice.CancelledCallInvite; +import com.twilio.voice.ConnectOptions; import com.twilio.voice.LogLevel; import com.twilio.voice.RegistrationException; import com.twilio.voice.RegistrationListener; @@ -59,6 +62,8 @@ import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_DEVICE_DID_RECEIVE_INCOMING; import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_DEVICE_NOT_READY; import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_DEVICE_READY; +import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CALL_STATE_RINGING; +import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CALL_INVITE_CANCELLED; public class TwilioVoiceModule extends ReactContextBaseJavaModule implements ActivityEventListener, LifecycleEventListener { @@ -78,6 +83,8 @@ 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_FCM_TOKEN = "com.hoxfon.react.TwilioVoice.ACTION_FCM_TOKEN"; @@ -85,6 +92,7 @@ public class TwilioVoiceModule extends ReactContextBaseJavaModule implements Act public static final String ACTION_ANSWER_CALL = "com.hoxfon.react.TwilioVoice.ANSWER_CALL"; public static final String ACTION_REJECT_CALL = "com.hoxfon.react.TwilioVoice.REJECT_CALL"; public static final String ACTION_HANGUP_CALL = "com.hoxfon.react.TwilioVoice.HANGUP_CALL"; + public static final String ACTION_CANCEL_CALL_INVITE = "com.hoxfon.react.TwilioVoice.CANCEL_CALL_INVITE"; public static final String ACTION_CLEAR_MISSED_CALLS_COUNT = "com.hoxfon.react.TwilioVoice.CLEAR_MISSED_CALLS_COUNT"; public static final String CALL_SID_KEY = "CALL_SID"; @@ -217,6 +225,34 @@ 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) { + // TODO test this with JS app + if (BuildConfig.DEBUG) { + Log.d(TAG, "CALL RINGING callListener().onRinging call state = "+call.getState()); + Log.d(TAG, call.toString()); + } + WritableMap params = Arguments.createMap(); + if (call != null) { + params.putString("call_sid", call.getSid()); + params.putString("call_from", call.getFrom()); + params.putString("call_state", call.getState().name()); + } + eventManager.sendEvent(EVENT_CALL_STATE_RINGING, params); + } @Override public void onConnected(Call call) { if (BuildConfig.DEBUG) { @@ -245,6 +281,27 @@ public void onConnected(Call call) { eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params); } +// Prepare methods Twilio for v4 +// @Override +// public void onReconnecting(Call call, CallException callException) { +// if (BuildConfig.DEBUG) { +// Log.d(TAG, "CALL RECONNECTING callListener().onReconnecting call state = "+call.getState()); +// } +// // TODO implement +//// eventManager.sendEvent(XXX, params); +// +// } +// +// @Override +// public void onReconnected(Call call) { +// if (BuildConfig.DEBUG) { +// Log.d(TAG, "CALL RECONNECTED callListener().onReconnected call state = "+call.getState()); +// } +// +// // TODO implement +//// eventManager.sendEvent(XXX, params); +// } + @Override public void onDisconnected(Call call, CallException error) { unsetAudioFocus(); @@ -319,6 +376,7 @@ private void registerReceiver() { if (!isReceiverRegistered) { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ACTION_INCOMING_CALL); + intentFilter.addAction(ACTION_CANCEL_CALL_INVITE); intentFilter.addAction(ACTION_MISSED_CALL); LocalBroadcastManager.getInstance(getReactApplicationContext()).registerReceiver( voiceBroadcastReceiver, intentFilter); @@ -381,19 +439,13 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } private void handleIncomingCallIntent(Intent intent) { - if (intent == null || intent.getAction() == null) { - Log.e(TAG, "handleIncomingCallIntent intent is null"); - return; - } - if (intent.getAction().equals(ACTION_INCOMING_CALL)) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "handleIncomingCallIntent"); + } activeCallInvite = intent.getParcelableExtra(INCOMING_CALL_INVITE); - - if (activeCallInvite != null && (activeCallInvite.getState() == CallInvite.State.PENDING)) { + if (activeCallInvite != null) { callAccepted = false; - if (BuildConfig.DEBUG) { - Log.d(TAG, "handleIncomingCallIntent state = PENDING"); - } SoundPoolManager.getInstance(getReactApplicationContext()).playRinging(); if (getReactApplicationContext().getCurrentActivity() != null) { @@ -411,58 +463,36 @@ private void handleIncomingCallIntent(Intent intent) { 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()); + params.putString("call_to", activeCallInvite.getTo()); // this is not needed + // TODO check if this is needed + // params.putString("call_state", "PENDING"); eventManager.sendEvent(EVENT_DEVICE_DID_RECEIVE_INCOMING, params); } - - } else { + // TODO evaluate what more is needed at this point? + Log.e(TAG, "ACTION_INCOMING_CALL but not active call"); + } + } else if (intent.getAction().equals(ACTION_CANCEL_CALL_INVITE)) { + SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "activeCallInvite was cancelled by " + activeCallInvite.getFrom()); + } + if (!callAccepted) { 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(); - - // the call is not active yet - if (activeCall == null) { - - 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); - } + Log.d(TAG, "creating a missed call"); } - if (BuildConfig.DEBUG) { - Log.d(TAG, "====> END"); + 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", Call.State.DISCONNECTED.toString()); + eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); } } + clearIncomingNotification(activeCallInvite.getCallSid()); } else if (intent.getAction().equals(ACTION_FCM_TOKEN)) { if (BuildConfig.DEBUG) { Log.d(TAG, "handleIncomingCallIntent ACTION_FCM_TOKEN"); @@ -476,11 +506,21 @@ private class VoiceBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "VoiceBroadcastReceiver.onReceive "+action+". Intent "+ intent.getExtras()); + } if (action.equals(ACTION_INCOMING_CALL)) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "VoiceBroadcastReceiver.onReceive ACTION_INCOMING_CALL. Intent "+ intent.getExtras()); - } handleIncomingCallIntent(intent); + } else if (action.equals(ACTION_CANCEL_CALL_INVITE)) { + CancelledCallInvite cancelledCallInvite = intent.getParcelableExtra(CANCELLED_CALL_INVITE); + clearIncomingNotification(cancelledCallInvite.getCallSid()); + WritableMap params = Arguments.createMap(); + if (cancelledCallInvite != null) { + params.putString("call_sid", cancelledCallInvite.getCallSid()); + params.putString("call_from", cancelledCallInvite.getFrom()); + params.putString("call_to", cancelledCallInvite.getTo()); + } + eventManager.sendEvent(EVENT_CALL_INVITE_CANCELLED, params); } else if (action.equals(ACTION_MISSED_CALL)) { SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences(PREFERENCE_KEY, Context.MODE_PRIVATE); SharedPreferences.Editor sharedPrefEditor = sharedPref.edit(); @@ -514,21 +554,19 @@ public void initWithAccessToken(final String accessToken, Promise promise) { promise.resolve(params); } - private void clearIncomingNotification(CallInvite callInvite) { + private void clearIncomingNotification(String callSid) { if (BuildConfig.DEBUG) { - Log.d(TAG, "clearIncomingNotification() callInvite state: "+ callInvite.getState()); + Log.d(TAG, "clearIncomingNotification() callSid: "+ callSid); } - if (callInvite != null && callInvite.getCallSid() != null) { - // remove incoming call notification - String notificationKey = INCOMING_NOTIFICATION_PREFIX + callInvite.getCallSid(); - int notificationId = 0; - if (TwilioVoiceModule.callNotificationMap.containsKey(notificationKey)) { - notificationId = TwilioVoiceModule.callNotificationMap.get(notificationKey); - } - callNotificationManager.removeIncomingCallNotification(getReactApplicationContext(), null, notificationId); - TwilioVoiceModule.callNotificationMap.remove(notificationKey); + // remove incoming call notification + String notificationKey = INCOMING_NOTIFICATION_PREFIX + callSid; + int notificationId = 0; + if (TwilioVoiceModule.callNotificationMap.containsKey(notificationKey)) { + notificationId = TwilioVoiceModule.callNotificationMap.get(notificationKey); } -// activeCallInvite = null; + callNotificationManager.removeIncomingCallNotification(getReactApplicationContext(), null, notificationId); + TwilioVoiceModule.callNotificationMap.remove(notificationKey); + activeCallInvite = null; } /* @@ -554,38 +592,36 @@ 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); } } }); - } @ReactMethod public void accept() { callAccepted = true; SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); - if (activeCallInvite != null){ - if (activeCallInvite.getState() == CallInvite.State.PENDING) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "accept() activeCallInvite.getState() PENDING"); - } - activeCallInvite.accept(getReactApplicationContext(), callListener); - clearIncomingNotification(activeCallInvite); - } 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); + if (activeCallInvite != null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "accept()"); } + AcceptOptions acceptOptions = new AcceptOptions.Builder().build(); + activeCallInvite.accept(getReactApplicationContext(), acceptOptions, callListener); + clearIncomingNotification(activeCallInvite.getCallSid()); + + // TODO check whether this block is needed +// // 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()); +// callNotificationManager.createHangupLocalNotification(getReactApplicationContext(), +// activeCallInvite.getCallSid(), +// activeCallInvite.getFrom()); +// eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params); } else { eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, null); } @@ -596,13 +632,15 @@ public void reject() { callAccepted = false; SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); WritableMap params = Arguments.createMap(); - if (activeCallInvite != null){ + if (activeCallInvite != null) { 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", "DISCONNECTED"); + // TODO check if DISCONNECTED should be REJECTED + // params.putString("call_state", "REJECTED"); activeCallInvite.reject(getReactApplicationContext()); - clearIncomingNotification(activeCallInvite); + clearIncomingNotification(activeCallInvite.getCallSid()); } eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); } @@ -612,12 +650,12 @@ public void ignore() { callAccepted = false; SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); WritableMap params = Arguments.createMap(); - if (activeCallInvite != null){ + if (activeCallInvite != null) { 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()); - clearIncomingNotification(activeCallInvite); + params.putString("call_state", "BUSY"); + clearIncomingNotification(activeCallInvite.getCallSid()); } eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); } @@ -673,7 +711,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 @@ -705,22 +747,30 @@ public void getActiveCall(Promise promise) { Log.d(TAG, "Active call found state = "+activeCall.getState()); } WritableMap params = Arguments.createMap(); + String toNum = activeCall.getTo(); + if (toNum == null) { + toNum = toNumber; + } params.putString("call_sid", activeCall.getSid()); params.putString("call_from", activeCall.getFrom()); - params.putString("call_to", activeCall.getTo()); + params.putString("call_to", toNum); params.putString("call_state", activeCall.getState().name()); promise.resolve(params); return; } + promise.resolve(null); + } + + @ReactMethod + public void getCallInvite(Promise promise) { if (activeCallInvite != null) { if (BuildConfig.DEBUG) { - Log.d(TAG, "Active call invite found state = "+activeCallInvite.getState()); + Log.d(TAG, "Call invite found "+ activeCallInvite); } 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()); promise.resolve(params); return; } @@ -734,8 +784,17 @@ public void setSpeakerPhone(Boolean value) { audioManager.setSpeakerphoneOn(value); } + @ReactMethod + public void setOnHold(Boolean value) { + if (activeCall != null) { + activeCall.hold(value); + } + } + private void setAudioFocus() { if (audioManager == null) { + audioManager.setMode(originalAudioMode); + audioManager.abandonAudioFocus(null); return; } originalAudioMode = audioManager.getMode(); @@ -755,11 +814,12 @@ public void onAudioFocusChange(int i) { } .build(); audioManager.requestAudioFocus(focusRequest); } else { - audioManager.requestAudioFocus( - null, - AudioManager.STREAM_VOICE_CALL, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE - ); + int focusRequestResult = audioManager.requestAudioFocus(new AudioManager.OnAudioFocusChangeListener() { + @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 @@ -772,6 +832,8 @@ public void onAudioFocusChange(int i) { } private void unsetAudioFocus() { if (audioManager == null) { + audioManager.setMode(originalAudioMode); + audioManager.abandonAudioFocus(null); return; } audioManager.setMode(originalAudioMode); 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..520b77f1 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 @@ -1,7 +1,5 @@ package com.hoxfon.react.RNTwilioVoice.fcm; -import android.annotation.TargetApi; - import android.app.ActivityManager; import android.content.Intent; import android.os.Handler; @@ -19,7 +17,7 @@ import com.hoxfon.react.RNTwilioVoice.BuildConfig; import com.hoxfon.react.RNTwilioVoice.CallNotificationManager; 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 +27,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_INVITE; 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; @@ -71,8 +71,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(data, new MessageListener() { @Override public void onCallInvite(final CallInvite callInvite) { @@ -102,7 +101,10 @@ public void run() { if (appImportance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { context.startActivity(launchIntent); } - VoiceFirebaseMessagingService.this.handleIncomingCall((ReactApplicationContext)context, notificationId, callInvite, launchIntent); + Intent intent = new Intent(ACTION_INCOMING_CALL); + intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); + intent.putExtra(INCOMING_CALL_INVITE, callInvite); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); } else { // Otherwise wait for construction, then handle the incoming call mReactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() { @@ -113,7 +115,13 @@ public void onReactContextInitialized(ReactContext context) { } Intent launchIntent = callNotificationManager.getLaunchIntent((ReactApplicationContext)context, notificationId, callInvite, true, appImportance); context.startActivity(launchIntent); - VoiceFirebaseMessagingService.this.handleIncomingCall((ReactApplicationContext)context, notificationId, callInvite, launchIntent); + Intent intent = new Intent(ACTION_INCOMING_CALL); + intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); + intent.putExtra(INCOMING_CALL_INVITE, callInvite); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + callNotificationManager.createIncomingCallNotification( + (ReactApplicationContext) context, callInvite, notificationId, + launchIntent); } }); if (!mReactInstanceManager.hasStartedCreatingInitialContext()) { @@ -126,10 +134,19 @@ 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) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + public void run() { + VoiceFirebaseMessagingService.this.sendCancelledCallInviteToActivity(cancelledCallInvite); + } + }); } }); + + if (!valid) { + Log.e(TAG, "The message was not a valid Twilio Voice SDK payload: " + remoteMessage.getData()); + } } // Check if message contains a notification payload. @@ -138,43 +155,13 @@ public void onError(MessageException messageException) { } } - private void handleIncomingCall(ReactApplicationContext context, - int notificationId, - CallInvite callInvite, - Intent launchIntent - ) { - sendIncomingCallMessageToActivity(context, callInvite, notificationId); - showNotification(context, callInvite, notificationId, launchIntent); - } - - /* - * Send the IncomingCallMessage to the TwilioVoiceModule - */ - private void sendIncomingCallMessageToActivity( - ReactApplicationContext context, - CallInvite callInvite, - int notificationId - ) { - Intent intent = new Intent(ACTION_INCOMING_CALL); - intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); - intent.putExtra(INCOMING_CALL_INVITE, callInvite); - LocalBroadcastManager.getInstance(context).sendBroadcast(intent); - } - /* - * Show the notification in the Android notification drawer + * Send the CancelledCallInvite to the TwilioVoiceModule */ - @TargetApi(20) - private void showNotification(ReactApplicationContext context, - CallInvite callInvite, - 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); - } + private void sendCancelledCallInviteToActivity(CancelledCallInvite cancelledCallInvite) { + SoundPoolManager.getInstance((this)).stopRinging(); + Intent intent = new Intent(ACTION_CANCEL_CALL_INVITE); + intent.putExtra(CANCELLED_CALL_INVITE, cancelledCallInvite); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); } } diff --git a/index.js b/index.js index 79dd8841..4c3f40c3 100644 --- a/index.js +++ b/index.js @@ -17,7 +17,8 @@ const _eventHandlers = { deviceDidReceiveIncoming: new Map(), connectionDidConnect: new Map(), connectionDidDisconnect: new Map(), - //iOS specific + callStateRinging: new Map(), + callInviteCancelled: new Map(), callRejected: new Map(), } @@ -52,9 +53,7 @@ const Twilio = { connect(params = {}) { TwilioVoice.connect(params) }, - disconnect() { - TwilioVoice.disconnect() - }, + disconnect: TwilioVoice.disconnect, accept() { if (Platform.OS === IOS) { return @@ -73,23 +72,17 @@ const Twilio = { } TwilioVoice.ignore() }, - setMuted(isMuted) { - TwilioVoice.setMuted(isMuted) - }, - setSpeakerPhone(value) { - TwilioVoice.setSpeakerPhone(value) - }, - sendDigits(digits) { - TwilioVoice.sendDigits(digits) - }, + setMuted: TwilioVoice.setMuted, + setSpeakerPhone: TwilioVoice.setSpeakerPhone, + sendDigits: TwilioVoice.sendDigits, + hold: TwilioVoice.hold, requestPermissions(senderId) { if (Platform.OS === ANDROID) { TwilioVoice.requestPermissions(senderId) } }, - getActiveCall() { - return TwilioVoice.getActiveCall() - }, + getActiveCall: TwilioVoice.getActiveCall, + getCallInvite: TwilioVoice.getCallInvite, configureCallKit(params = {}) { if (Platform.OS === IOS) { TwilioVoice.configureCallKit(params) @@ -101,6 +94,10 @@ const Twilio = { } }, addEventListener(type, handler) { + if (!_eventHandlers.hasOwnProperty(type)) { + throw new Error('Event handler not found: ' + type) + } + if (_eventHandlers[type]) if (_eventHandlers[type].has(handler)) { return } From 9b6a148c0db32e882250e05ebb52c685e4925bcd Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sun, 12 Jan 2020 19:02:52 +0000 Subject: [PATCH 14/19] formatting --- .../RNTwilioVoice/TwilioVoiceModule.java | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) 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 09573c77..3aa79403 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java @@ -253,6 +253,7 @@ public void onRinging(Call call) { } eventManager.sendEvent(EVENT_CALL_STATE_RINGING, params); } + @Override public void onConnected(Call call) { if (BuildConfig.DEBUG) { @@ -304,15 +305,14 @@ public void onConnected(Call call) { @Override public void onDisconnected(Call call, CallException error) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "CALL DISCONNECTED callListener().onDisconnected call state = "+call.getState()); + } unsetAudioFocus(); proximityManager.stopProximitySensor(); headsetManager.stopWiredHeadsetEvent(getReactApplicationContext()); callAccepted = false; - if (BuildConfig.DEBUG) { - Log.d(TAG, "call disconnected"); - } - WritableMap params = Arguments.createMap(); String callSid = ""; if (call != null) { @@ -338,14 +338,15 @@ public void onDisconnected(Call call, CallException error) { @Override public void onConnectFailure(Call call, CallException error) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "CALL FAILURE callListener().onConnectFailure call state = "+call.getState()); + } unsetAudioFocus(); proximityManager.stopProximitySensor(); callAccepted = false; - if (BuildConfig.DEBUG) { - Log.d(TAG, "connect failure"); - } - Log.e(TAG, String.format("CallListener onDisconnected error: %d, %s", + + Log.e(TAG, String.format("CallListener onConnectFailure error: %d, %s", error.getErrorCode(), error.getMessage())); WritableMap params = Arguments.createMap(); @@ -727,9 +728,9 @@ public void disconnect() { } @ReactMethod - public void setMuted(Boolean muteValue) { + public void setMuted(Boolean value) { if (activeCall != null) { - activeCall.mute(muteValue); + activeCall.mute(value); } } From b334a6cc32e2dde01a8b9d6e21447f2ba2c99a0c Mon Sep 17 00:00:00 2001 From: Ani Ravi <5902976+aniravi24@users.noreply.github.com> Date: Sun, 5 Jan 2020 13:14:04 -0700 Subject: [PATCH 15/19] readme updates for RN 0.60 autolink --- README.md | 30 +++++++++---------- .../RNTwilioVoice/TwilioVoiceModule.java | 4 +-- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 88ff2846..203fd6b5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # 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 @@ -8,7 +9,9 @@ This is a React Native wrapper for Twilio Programmable Voice SDK that lets you m ## Breaking changes in v4.0.0 -- Android: remove the following block from your application's `AndroidManifest.xml` +It implements [react-native autolinking](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md) as many other native libraries > react-native 0.60.0 + +Android: update Firebase Messaging to 17.3.4. Remove the following block from your application's `AndroidManifest.xml` ```xml ``` +Android X is supported. + Data passed to the event `deviceDidReceiveIncoming` does not contain the key `call_state`, because state of Call Invites was removed in Twilio Android v3.0.0 - iOS: params changes for `connectionDidConnect` and `connectionDidDisconnect` @@ -57,7 +62,9 @@ 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). + +### Manual Linking ``` @@ -140,8 +147,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 @@ -168,22 +176,12 @@ In your `AndroidManifest.xml` - - - - - - - - ..... ``` -In `android/settings.gradle` +In `android/settings.gradle` (not necessary if auto-linking on RN 0.60+) ```gradle ... @@ -192,7 +190,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/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java index 3aa79403..0d9997e8 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java @@ -464,9 +464,7 @@ private void handleIncomingCallIntent(Intent intent) { WritableMap params = Arguments.createMap(); params.putString("call_sid", activeCallInvite.getCallSid()); params.putString("call_from", activeCallInvite.getFrom()); - params.putString("call_to", activeCallInvite.getTo()); // this is not needed - // TODO check if this is needed - // params.putString("call_state", "PENDING"); + params.putString("call_to", activeCallInvite.getTo()); // TODO check if needed eventManager.sendEvent(EVENT_DEVICE_DID_RECEIVE_INCOMING, params); } } else { From f1db444f33b80b7124c4468f8b9c54a902c7c20e Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sat, 25 Jan 2020 18:25:02 +0000 Subject: [PATCH 16/19] Android: implement Twilio Programmable Voice Android SDK 4.5.0 - handle Call `onReconnecting` and Call `onReconnected` --- README.md | 10 ++- android/build.gradle | 8 +-- .../react/RNTwilioVoice/EventManager.java | 2 + .../RNTwilioVoice/TwilioVoiceModule.java | 64 ++++++++++++------- index.js | 2 + 5 files changed, 58 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 203fd6b5..fc3977c0 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,14 @@ This is a React Native wrapper for Twilio Programmable Voice SDK that lets you m # Twilio Programmable Voice SDK -- Android 3.3.0 (bundled within this library) +- Android 4.5.0 (bundled within this library) - iOS 2.1.0 (specified by the app's own podfile) ## Breaking changes in v4.0.0 It implements [react-native autolinking](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md) as many other native libraries > react-native 0.60.0 -Android: update Firebase Messaging to 17.3.4. Remove the following block from your application's `AndroidManifest.xml` +Android: update Firebase Messaging to 17.6.+. Remove the following block from your application's `AndroidManifest.xml` ```xml Date: Thu, 13 Feb 2020 10:31:42 +0000 Subject: [PATCH 17/19] iOS: implement Twilio Voice SDK v5.2.0 --- README.md | 8 +- RNTwilioVoice.podspec | 6 +- index.js | 5 - ios/RNTwilioVoice.xcodeproj/project.pbxproj | 24 +- .../xcshareddata/IDEWorkspaceChecks.plist | 8 + ios/RNTwilioVoice/RNTwilioVoice.m | 642 ++++++++++++------ 6 files changed, 439 insertions(+), 254 deletions(-) create mode 100644 ios/RNTwilioVoice.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/README.md b/README.md index fc3977c0..bc6a6eb8 100644 --- a/README.md +++ b/README.md @@ -82,11 +82,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 @@ -101,11 +101,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 diff --git a/RNTwilioVoice.podspec b/RNTwilioVoice.podspec index 9b80fac6..4fa8df77 100644 --- a/RNTwilioVoice.podspec +++ b/RNTwilioVoice.podspec @@ -9,12 +9,12 @@ 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"} 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 +end diff --git a/index.js b/index.js index 9b3b5ee0..2c897288 100644 --- a/index.js +++ b/index.js @@ -47,11 +47,6 @@ const Twilio = { } return result }, - initWithTokenUrl(url) { - if (Platform.OS === IOS) { - TwilioVoice.initWithAccessTokenUrl(url) - } - }, connect(params = {}) { TwilioVoice.connect(params) }, diff --git a/ios/RNTwilioVoice.xcodeproj/project.pbxproj b/ios/RNTwilioVoice.xcodeproj/project.pbxproj index 92ab77a4..a7be7718 100644 --- a/ios/RNTwilioVoice.xcodeproj/project.pbxproj +++ b/ios/RNTwilioVoice.xcodeproj/project.pbxproj @@ -7,26 +7,10 @@ objects = { /* Begin PBXBuildFile section */ - 016DD9B51ECCA23A00315CD4 /* TwilioVoice.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 016DD9B41ECCA23A00315CD4 /* TwilioVoice.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 38F71DFA1E9C333F0067E86F /* RNTwilioVoice.m in Sources */ = {isa = PBXBuildFile; fileRef = 38F71DF91E9C333F0067E86F /* RNTwilioVoice.m */; }; - 38F71DFB1E9C333F0067E86F /* RNTwilioVoice.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 38F71DF81E9C333F0067E86F /* RNTwilioVoice.h */; }; /* End PBXBuildFile section */ -/* Begin PBXCopyFilesBuildPhase section */ - 38F71DF31E9C333F0067E86F /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "include/$(PRODUCT_NAME)"; - dstSubfolderSpec = 16; - files = ( - 38F71DFB1E9C333F0067E86F /* RNTwilioVoice.h in CopyFiles */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXFileReference section */ - 016DD9B41ECCA23A00315CD4 /* TwilioVoice.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TwilioVoice.framework; path = "../../../Downloads/twilio-voice-ios/TwilioVoice.framework"; sourceTree = ""; }; 38F71DF51E9C333F0067E86F /* libRNTwilioVoice.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNTwilioVoice.a; sourceTree = BUILT_PRODUCTS_DIR; }; 38F71DF81E9C333F0067E86F /* RNTwilioVoice.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNTwilioVoice.h; sourceTree = ""; }; 38F71DF91E9C333F0067E86F /* RNTwilioVoice.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNTwilioVoice.m; sourceTree = ""; }; @@ -37,7 +21,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 016DD9B51ECCA23A00315CD4 /* TwilioVoice.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -47,7 +30,6 @@ 016DD9801ECC9B4B00315CD4 /* Frameworks */ = { isa = PBXGroup; children = ( - 016DD9B41ECCA23A00315CD4 /* TwilioVoice.framework */, ); name = Frameworks; sourceTree = ""; @@ -86,7 +68,6 @@ buildConfigurationList = 38F71DFE1E9C333F0067E86F /* Build configuration list for PBXNativeTarget "RNTwilioVoice" */; buildPhases = ( 38F71DF11E9C333F0067E86F /* Sources */, - 38F71DF31E9C333F0067E86F /* CopyFiles */, 016DD9B31ECCA22F00315CD4 /* Frameworks */, ); buildRules = ( @@ -118,6 +99,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = 38F71DEC1E9C333E0067E86F; @@ -191,7 +173,7 @@ "${SRCROOT}/../../../ios/Pods/Headers/Public", "${SRCROOT}/../../../ios/Pods/Headers/Public/TwilioVoice", ); - IPHONEOS_DEPLOYMENT_TARGET = 10.2; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -241,7 +223,7 @@ "${SRCROOT}/../../../ios/Pods/Headers/Public", "${SRCROOT}/../../../ios/Pods/Headers/Public/TwilioVoice", ); - IPHONEOS_DEPLOYMENT_TARGET = 10.2; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/ios/RNTwilioVoice.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/RNTwilioVoice.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/ios/RNTwilioVoice.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/RNTwilioVoice/RNTwilioVoice.m b/ios/RNTwilioVoice/RNTwilioVoice.m index 7fc54618..90ab70cf 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.m +++ b/ios/RNTwilioVoice/RNTwilioVoice.m @@ -10,15 +10,24 @@ @import CallKit; @import TwilioVoice; +NSString * const kCachedDeviceToken = @"CachedDeviceToken"; + @interface RNTwilioVoice () -@property (nonatomic, strong) NSString *deviceTokenString; @property (nonatomic, strong) PKPushRegistry *voipRegistry; +@property (nonatomic, strong) void(^incomingPushCompletionCallback)(void); @property (nonatomic, strong) TVOCallInvite *callInvite; -@property (nonatomic, strong) TVOCall *call; @property (nonatomic, strong) void(^callKitCompletionCallback)(BOOL); +@property (nonatomic, strong) TVODefaultAudioDevice *audioDevice; +@property (nonatomic, strong) NSMutableDictionary *activeCallInvites; +@property (nonatomic, strong) NSMutableDictionary *activeCalls; + +// activeCall represents the last connected call +@property (nonatomic, strong) TVOCall *activeCall; @property (nonatomic, strong) CXProvider *callKitProvider; @property (nonatomic, strong) CXCallController *callKitCallController; +@property (nonatomic, assign) BOOL userInitiatedDisconnect; + @end @implementation RNTwilioVoice { @@ -28,7 +37,6 @@ @implementation RNTwilioVoice { NSString *_token; } -NSString * const StatePending = @"PENDING"; NSString * const StateConnecting = @"CONNECTING"; NSString * const StateConnected = @"CONNECTED"; NSString * const StateDisconnected = @"DISCONNECTED"; @@ -43,7 +51,7 @@ - (dispatch_queue_t)methodQueue - (NSArray *)supportedEvents { - return @[@"connectionDidConnect", @"connectionDidDisconnect", @"callRejected", @"deviceReady", @"deviceNotReady"]; + return @[@"connectionDidConnect", @"connectionDidDisconnect", @"callRejected", @"deviceReady", @"deviceNotReady", @"deviceDidReceiveIncoming", @"callInviteCancelled", @"callStateRinging", @"connectionIsReconnecting", @"connectionDidReconnect"]; } @synthesize bridge = _bridge; @@ -62,14 +70,19 @@ - (void)dealloc { [self initPushRegistry]; } -RCT_EXPORT_METHOD(initWithAccessTokenUrl:(NSString *)tokenUrl) { - _tokenUrl = tokenUrl; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAppTerminateNotification) name:UIApplicationWillTerminateNotification object:nil]; - [self initPushRegistry]; -} - RCT_EXPORT_METHOD(configureCallKit: (NSDictionary *)params) { if (self.callKitCallController == nil) { + /* + * 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]; + _settings = [[NSMutableDictionary alloc] initWithDictionary:params]; CXProviderConfiguration *configuration = [[CXProviderConfiguration alloc] initWithLocalizedName:params[@"appName"]]; configuration.maximumCallGroups = 1; @@ -93,13 +106,11 @@ - (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; - if (self.call && self.call.state == TVOCallStateConnected) { - [self performEndCallActionWithUUID:self.call.uuid]; + if (self.activeCall && self.activeCall.state == TVOCallStateConnected) { + [self performEndCallActionWithUUID:self.activeCall.uuid]; } else { NSUUID *uuid = [NSUUID UUID]; NSString *handle = [params valueForKey:@"To"]; @@ -109,86 +120,87 @@ - (void)dealloc { } RCT_EXPORT_METHOD(disconnect) { - NSLog(@"Disconnecting call"); - [self performEndCallActionWithUUID:self.call.uuid]; + NSLog(@"Disconnecting call. UUID %@", self.activeCall.uuid.UUIDString); + self.userInitiatedDisconnect = YES; + [self performEndCallActionWithUUID:self.activeCall.uuid]; } RCT_EXPORT_METHOD(setMuted: (BOOL *)muted) { NSLog(@"Mute/UnMute call"); - self.call.muted = muted; + self.activeCall.muted = muted ? YES : NO; } RCT_EXPORT_METHOD(setSpeakerPhone: (BOOL *)speaker) { - [self toggleAudioRoute:speaker]; + [self toggleAudioRoute: speaker ? YES : NO]; } RCT_EXPORT_METHOD(sendDigits: (NSString *)digits) { - if (self.call && self.call.state == TVOCallStateConnected) { + if (self.activeCall && self.activeCall.state == TVOCallStateConnected) { NSLog(@"SendDigits %@", digits); - [self.call sendDigits:digits]; + [self.activeCall sendDigits:digits]; } } RCT_EXPORT_METHOD(unregister) { NSLog(@"unregister"); NSString *accessToken = [self fetchAccessToken]; - - [TwilioVoice unregisterWithAccessToken:accessToken - deviceToken:self.deviceTokenString - completion:^(NSError * _Nullable error) { - if (error) { - NSLog(@"An error occurred while unregistering: %@", [error localizedDescription]); - } else { - NSLog(@"Successfully unregistered for VoIP push notifications."); - } - }]; - - self.deviceTokenString = nil; + NSString *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken]; + if ([cachedDeviceToken length] > 0) { + [TwilioVoice unregisterWithAccessToken:accessToken + deviceToken:cachedDeviceToken + completion:^(NSError * _Nullable error) { + if (error) { + NSLog(@"An error occurred while unregistering: %@", [error localizedDescription]); + } else { + NSLog(@"Successfully unregistered for VoIP push notifications."); + } + }]; + } } RCT_REMAP_METHOD(getActiveCall, - 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.callInvite.to) { - [params setObject:self.callInvite.to forKey:@"to"]; - } - 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"]; + activeCallResolver:(RCTPromiseResolveBlock)resolve + activeCallRejecter:(RCTPromiseRejectBlock)reject) { + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (self.activeCall) { + if (self.activeCall.sid) { + [params setObject:self.activeCall.sid forKey:@"call_sid"]; + } + if (self.activeCall.to) { + [params setObject:self.activeCall.to forKey:@"call_to"]; + } + if (self.activeCall.from) { + [params setObject:self.activeCall.from forKey:@"call_from"]; + } + if (self.activeCall.state == TVOCallStateConnected) { + [params setObject:StateConnected forKey:@"call_state"]; + } else if (self.activeCall.state == TVOCallStateConnecting) { + [params setObject:StateConnecting forKey:@"call_state"]; + } else if (self.activeCall.state == TVOCallStateDisconnected) { + [params setObject:StateDisconnected forKey:@"call_state"]; + } } resolve(params); - } else if (self.call) { - 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"]; - } - if (self.call.state == TVOCallStateConnected) { - [params setObject:StateConnected forKey:@"call_state"]; - } else if (self.call.state == TVOCallStateConnecting) { - [params setObject:StateConnecting forKey:@"call_state"]; - } else if (self.call.state == TVOCallStateDisconnected) { - [params setObject:StateDisconnected forKey:@"call_state"]; +} + +RCT_REMAP_METHOD(getCallInvite, + callInvieteResolver:(RCTPromiseResolveBlock)resolve + callInviteRejecter:(RCTPromiseRejectBlock)reject) { + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (self.activeCallInvites.count) { + // considering only the first call invite + TVOCallInvite *callInvite = [self.activeCallInvites valueForKey:[self.activeCallInvites allKeys][self.activeCallInvites.count-1]]; + 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"]; + } } resolve(params); - } else{ - reject(@"no_call", @"There was no active call", nil); - } } - (void)initPushRegistry { @@ -214,27 +226,39 @@ - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPush if ([type isEqualToString:PKPushTypeVoIP]) { const unsigned *tokenBytes = [credentials.token bytes]; - self.deviceTokenString = [NSString stringWithFormat:@"<%08x %08x %08x %08x %08x %08x %08x %08x>", + NSString *deviceTokenString = [NSString stringWithFormat:@"<%08x %08x %08x %08x %08x %08x %08x %08x>", ntohl(tokenBytes[0]), ntohl(tokenBytes[1]), ntohl(tokenBytes[2]), ntohl(tokenBytes[3]), ntohl(tokenBytes[4]), ntohl(tokenBytes[5]), ntohl(tokenBytes[6]), ntohl(tokenBytes[7])]; - NSString *accessToken = [self fetchAccessToken]; - - [TwilioVoice registerWithAccessToken:accessToken - deviceToken:self.deviceTokenString - completion:^(NSError *error) { - if (error) { - NSLog(@"An error occurred while registering: %@", [error localizedDescription]); - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - [params setObject:[error localizedDescription] forKey:@"err"]; - - [self sendEventWithName:@"deviceNotReady" body:params]; - } else { - NSLog(@"Successfully registered for VoIP push notifications."); - [self sendEventWithName:@"deviceReady" body:nil]; - } - }]; + NSString *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken]; + if (![cachedDeviceToken isEqualToString:deviceTokenString]) { + cachedDeviceToken = deviceTokenString; + + /* + * Perform registration if a new device token is detected. + */ + [TwilioVoice registerWithAccessToken:accessToken + deviceToken:cachedDeviceToken + completion:^(NSError *error) { + if (error) { + NSLog(@"An error occurred while registering: %@", [error localizedDescription]); + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + [params setObject:[error localizedDescription] forKey:@"err"]; + + [self sendEventWithName:@"deviceNotReady" body:params]; + } + else { + NSLog(@"Successfully registered for VoIP push notifications."); + + /* + * Save the device token after successfully registered. + */ + [[NSUserDefaults standardUserDefaults] setObject:cachedDeviceToken forKey:kCachedDeviceToken]; + [self sendEventWithName:@"deviceReady" body:nil]; + } + }]; + } } } @@ -244,8 +268,10 @@ - (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(P if ([type isEqualToString:PKPushTypeVoIP]) { NSString *accessToken = [self fetchAccessToken]; - [TwilioVoice unregisterWithAccessToken:accessToken - deviceToken:self.deviceTokenString + NSString *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken]; + if ([cachedDeviceToken length] > 0) { + [TwilioVoice unregisterWithAccessToken:accessToken + deviceToken:cachedDeviceToken completion:^(NSError * _Nullable error) { if (error) { NSLog(@"An error occurred while unregistering: %@", [error localizedDescription]); @@ -253,81 +279,183 @@ - (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(P NSLog(@"Successfully unregistered for VoIP push notifications."); } }]; - - self.deviceTokenString = nil; + } } } +/** +* Try using the `pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:` method if +* your application is targeting iOS 11. According to the docs, this delegate method is deprecated by Apple. +*/ - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type { NSLog(@"pushRegistry:didReceiveIncomingPushWithPayload:forType"); - if ([type isEqualToString:PKPushTypeVoIP]) { - [TwilioVoice handleNotification:payload.dictionaryPayload - delegate:self]; + // 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."); + } } } -#pragma mark - TVONotificationDelegate -- (void)callInviteReceived:(TVOCallInvite *)callInvite { - if (callInvite.state == TVOCallInviteStatePending) { - [self handleCallInviteReceived:callInvite]; - } else if (callInvite.state == TVOCallInviteStateCanceled) { - [self handleCallInviteCanceled:callInvite]; - } -} +/** + * This delegate method is available on iOS 11 and above. Call the completion handler once the + * notification payload is passed to the `TwilioVoice.handleNotification()` method. + */ +- (void)pushRegistry:(PKPushRegistry *)registry +didReceiveIncomingPushWithPayload:(PKPushPayload *)payload + forType:(PKPushType)type +withCompletionHandler:(void (^)(void))completion { + NSLog(@"pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler"); -- (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; - } + // Save for later when the notification is properly handled. + self.incomingPushCompletionCallback = completion; - self.callInvite = callInvite; - [self reportIncomingCallFrom:callInvite.from withUUID:callInvite.uuid]; + if ([type isEqualToString:PKPushTypeVoIP]) { + // 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."); + } + } + 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(); + } } -- (void)handleCallInviteCanceled:(TVOCallInvite *)callInvite { - NSLog(@"callInviteCanceled"); +- (void)incomingPushHandled { + if (self.incomingPushCompletionCallback) { + self.incomingPushCompletionCallback(); + self.incomingPushCompletionCallback = nil; + } +} - [self performEndCallActionWithUUID:callInvite.uuid]; +#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 (self.callInvite.callSid) { - [params setObject:self.callInvite.callSid forKey:@"call_sid"]; - } + 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"]; + } + [self sendEventWithName:@"deviceDidReceiveIncoming" body:params]; +} + +- (void)cancelledCallInviteReceived:(nonnull TVOCancelledCallInvite *)cancelledCallInvite { + /** + * 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"]; + } + [self sendEventWithName:@"callInviteCancelled" body:params]; + } +} - 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]; - self.callInvite = nil; +- (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 with error"); + 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"]; + } + [self sendEventWithName:@"callInviteCancelled" body:params]; + } } - (void)notificationError:(NSError *)error { NSLog(@"notificationError: %@", [error localizedDescription]); } +#pragma mark - TVOCallDelegate +- (void)callDidStartRinging:(TVOCall *)call { + NSLog(@"callDidStartRinging"); + + /* + When [answerOnBridge](https://www.twilio.com/docs/voice/twiml/dial#answeronbridge) is enabled in the + TwiML verb, the caller will not hear the ringback while the call is ringing and awaiting to be + accepted on the callee's side. The application can use the `AVAudioPlayer` to play custom audio files + between the `[TVOCallDelegate callDidStartRinging:]` and the `[TVOCallDelegate callDidConnect:]` callbacks. + */ + NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; + [callParams setObject:call.sid forKey:@"call_sid"]; + if (call.from) { + [callParams setObject:call.from forKey:@"call_from"]; + } + [self sendEventWithName:@"callStateRinging" body:callParams]; +} + #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"]; @@ -346,71 +474,117 @@ - (void)callDidConnect:(TVOCall *)call { [self sendEventWithName:@"connectionDidConnect" body:callParams]; } +- (void)call:(TVOCall *)call isReconnectingWithError:(NSError *)error { + NSLog(@"Call is reconnecting"); + NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; + [callParams setObject:call.sid forKey:@"call_sid"]; + if (call.from) { + [callParams setObject:call.from forKey:@"call_from"]; + } + if (call.to) { + [callParams setObject:call.to forKey:@"call_to"]; + } + [self sendEventWithName:@"connectionIsReconnecting" body:callParams]; +} + +- (void)callDidReconnect:(TVOCall *)call { + NSLog(@"Call reconnected"); + NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; + [callParams setObject:call.sid forKey:@"call_sid"]; + if (call.from) { + [callParams setObject:call.from forKey:@"call_from"]; + } + if (call.to) { + [callParams setObject:call.to forKey:@"call_to"]; + } + [self sendEventWithName:@"connectionDidReconnect" body:callParams]; +} + - (void)call:(TVOCall *)call didFailToConnectWithError:(NSError *)error { NSLog(@"Call failed to connect: %@", error); self.callKitCompletionCallback(NO); - [self performEndCallActionWithUUID:call.uuid]; - [self callDisconnected:error]; + [self performEndCallActionWithUUID:call.uuid]; + [self callDisconnected:call error:error]; } - (void)call:(TVOCall *)call didDisconnectWithError:(NSError *)error { - NSLog(@"Call disconnected with error: %@", error); + if (error) { + NSLog(@"didDisconnectWithError: %@", error); + } else { + NSLog(@"didDisconnect"); + } - [self callDisconnected:error]; + if (!self.userInitiatedDisconnect) { + CXCallEndedReason reason = CXCallEndedReasonRemoteEnded; + if (error) { + reason = CXCallEndedReasonFailed; + } + [self.callKitProvider reportCallWithUUID:call.uuid endedAtDate:[NSDate date] reason:reason]; + } + [self callDisconnected:call error:error]; } -- (void)callDisconnected:(NSError *)error { - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - if (error) { - NSString* errMsg = [error localizedDescription]; - if (error.localizedFailureReason) { - errMsg = [error localizedFailureReason]; +- (void)callDisconnected:(TVOCall *)call error:(NSError *)error { + NSLog(@"callDisconnect"); + if ([call isEqual:self.activeCall]) { + self.activeCall = nil; } - [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"]; - } - if (self.call.state == TVOCallStateDisconnected) { - [params setObject:StateDisconnected forKey:@"call_state"]; - } - [self sendEventWithName:@"connectionDidDisconnect" body:params]; + [self.activeCalls removeObjectForKey:call.uuid.UUIDString]; - self.call = nil; - self.callKitCompletionCallback = nil; -} + self.userInitiatedDisconnect = NO; -#pragma mark - AVAudioSession -- (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]); + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (error) { + NSString* errMsg = [error localizedDescription]; + if (error.localizedFailureReason) { + errMsg = [error localizedFailureReason]; + } + [params setObject:errMsg forKey:@"err"]; } - } else { - if (![[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone - error:&error]) { - NSLog(@"Unable to reroute audio: %@", [error localizedDescription]); + 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 { + // 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. + 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 +593,12 @@ - (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; + self.audioDevice.enabled = NO; } - (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action { @@ -434,8 +608,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 +628,8 @@ - (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."); - - TwilioVoice.audioEnabled = NO; + self.audioDevice.enabled = NO; + self.audioDevice.block(); [self performAnswerVoiceCallWithUUID:action.callUUID completion:^(BOOL success) { if (success) { [action fulfill]; @@ -476,28 +644,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) { - [self sendEventWithName:@"callRejected" body:@"callRejected"]; - [self.callInvite reject]; - self.callInvite = nil; - } else if (self.call) { - [self.call disconnect]; - } + if (callInvite) { + [callInvite reject]; + [self sendEventWithName:@"callRejected" body:@"callRejected"]; + [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 +724,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]); } @@ -554,7 +734,6 @@ - (void)performEndCallActionWithUUID:(NSUUID *)uuid { if (uuid == nil) { return; } - UIDevice* device = [UIDevice currentDevice]; device.proximityMonitoringEnabled = NO; @@ -564,8 +743,6 @@ - (void)performEndCallActionWithUUID:(NSUUID *)uuid { [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) { if (error) { NSLog(@"EndCallAction transaction request failed: %@", [error localizedDescription]); - } else { - NSLog(@"EndCallAction transaction request successful"); } }]; } @@ -573,29 +750,52 @@ - (void)performEndCallActionWithUUID:(NSUUID *)uuid { - (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.activeCall = call; + self.activeCalls[call.uuid.UUIDString] = call; + } self.callKitCompletionCallback = completionHandler; } - (void)performAnswerVoiceCallWithUUID:(NSUUID *)uuid completion:(void(^)(BOOL success))completionHandler { - self.call = [self.callInvite acceptWithDelegate:self]; - self.callInvite = nil; - self.callKitCompletionCallback = 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; + }]; + + TVOCall *call = [callInvite acceptWithOptions:acceptOptions delegate:self]; + + if (!call) { + completionHandler(NO); + } else { + self.callKitCompletionCallback = completionHandler; + self.activeCall = call; + self.activeCalls[call.uuid.UUIDString] = call; + } + + [self.activeCallInvites removeObjectForKey:callInvite.uuid.UUIDString]; + + if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) { + [self incomingPushHandled]; + } } - (void)handleAppTerminateNotification { NSLog(@"handleAppTerminateNotification called"); - if (self.call) { + if (self.activeCall) { NSLog(@"handleAppTerminateNotification disconnecting an active call"); - [self.call disconnect]; + [self.activeCall disconnect]; } } From f2406e6cd1089a3be768bccfe23c94fb347fc0dd Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sun, 16 Feb 2020 17:08:18 +0000 Subject: [PATCH 18/19] doc: update README --- README.md | 222 +++++++++++++++++++++++++++--------------------------- 1 file changed, 112 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index bc6a6eb8..a0b7e2b4 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ # 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 React Native App. This module is not affiliated with or maintained by the Twilio team. This is maintained by contributions from the community. +This is a React-Native wrapper for [Twilio Programmable Voice SDK](https://www.twilio.com/voice) which lets you make and receive calls from your React-Native App. This module is not affiliated with nor officially maintained by Twilio, and it is maintained by open source contributors. -# Twilio Programmable Voice SDK +## Twilio Programmable Voice SDK -- Android 4.5.0 (bundled within this library) -- iOS 2.1.0 (specified by the app's own podfile) +- Android 4.5.0 (bundled within the module) +- iOS 5.1.0 (specified by the app's own podfile) ## Breaking changes in v4.0.0 -It implements [react-native autolinking](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md) as many other native libraries > react-native 0.60.0 +The module implements [react-native autolinking](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md) as many other native libraries > react-native 0.60.0, therefore it doesn't need to be linked manually. + +Android: update Firebase Messaging to 17.6.+. Remove the following block from your application's `AndroidManifest.xml` if you are migrating from v3. -Android: update Firebase Messaging to 17.6.+. Remove the following block from your application's `AndroidManifest.xml` ```xml `call_to` move property `from` => `call_from` -## Help wanted! - -There is no need to ask permissions to contribute. Just open an issue or provide a PR. Everybody is welcome to contribute. - -ReactNative success is directly linked to its module ecosystem. One way to make an impact is helping contributing to this module or another of the many community lead ones. - -![help wanted](images/vjeux_tweet.png "help wanted") - ## 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. On RN 0.60+, this module can be auto-linked (Android still requires FCM setup below). - -### Manual Linking +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. - -``` +```bash npm install react-native-twilio-programmable-voice --save -react-native link react-native-twilio-programmable-voice ``` -### iOS Installation - when projects made with react-native init -After you have linked the library with `react-native link react-native-twilio-programmable-voice` -check that `libRNTwilioVoice.a` is present under YOUR_TARGET > Build Phases > Link Binaries With Libraries. If it is not present you can add it using the + sign at the bottom of that list. +- **React Native 0.60+** -Edit your `Podfile` to include TwilioVoice framework +[CLI autolink feature](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md) links the module while building the app. + +- **React Native <= 0.59** +```bash +react-native link react-native-twilio-programmable-voice ``` -source 'https://github.com/cocoapods/specs' -# min version for TwilioVoice to work -platform :ios, '10.0' +### iOS Installation -target do - ... - pod 'TwilioVoice', '~> 5.1.1' - ... -end +If you can't or don't want to use autolink, you can also manually link the library using the instructions below (click on the arrow to show them): -``` +
+Manually link the library on iOS -run `pod install` from inside your project `ios` directory +Follow the [instructions in the React Native documentation](https://facebook.github.io/react-native/docs/linking-libraries-ios#manual-linking) to manually link the framework -### iOS Installation - when projects made without react-native init -Edit your `Podfile` to include TwilioVoice and RNTwilioVoice frameworks +After you have linked the library with `react-native link react-native-twilio-programmable-voice` +check that `libRNTwilioVoice.a` is present under YOUR_TARGET > Build Phases > Link Binaries With Libraries. If it is not present you can add it using the + sign at the bottom of that list. +
-``` +Edit your `Podfile` to include TwilioVoice framework + +```ruby source 'https://github.com/cocoapods/specs' # min version for TwilioVoice to work @@ -105,24 +102,22 @@ platform :ios, '10.0' target do ... - pod 'TwilioVoice', '~> 5.1.1' - pod 'RNTwilioVoice', path: '../node_modules/react-native-twilio-programmable-voice' + pod 'TwilioVoice', '~> 5.2.0' ... end - ``` -run `pod install` from inside your project `ios` directory - -### CallKit +```bash +cd ios/ && pod install +``` -The current iOS part of this library works through [CallKit](https://developer.apple.com/reference/callkit). Because of this the call flow is much simpler than on Android as CallKit handles the inbound calls answering, ignoring, or rejecting. -Because of CallKit, the only event listeners present are "deviceReady", "connectionDidConnect", "connectionDidDisconnect", and "callRejected". +#### CallKit -### VoIP Service Certificate +The iOS library works through [CallKit](https://developer.apple.com/reference/callkit) and handling calls is much simpler than the Android implementation as CallKit handles the inbound calls answering, ignoring, or rejecting. Outbound calls must be controlled by custom React-Native screens and controls. -Twilio Programmable Voice for iOS utilizes Apple's VoIP Services and VoIP "Push Notifications" instead of FCM. You will need a VoIP Service Certificate from Apple to receive calls. +#### VoIP Service Certificate +Twilio Programmable Voice for iOS utilizes Apple's VoIP Services and VoIP "Push Notifications" instead of FCM. You will need a VoIP Service Certificate from Apple to receive calls. Follow [the official Twilio instructions](https://github.com/twilio/voice-quickstart-ios#7-create-voip-service-certificate) to complete this step. ## Android Installation @@ -131,42 +126,27 @@ Setup FCM You must download the file `google-services.json` from the Firebase console. It contains keys and settings for all your applications under Firebase. This library obtains the resource `senderID` for registering for remote GCM from that file. -**NOTE: To use a specific `play-service-gcm` version, update the `compile` instruction in your App's `android/app/build.gradle` (replace `10.+` with the version you prefer):** - -```gradle -... +#### `android/build.gradle` +```groovy buildscript { - ... - dependencies { - classpath 'com.google.gms:google-services:4.2.0' - } -} - -... - -dependencies { - ... - // on React Native 0.60+, this module can be auto-linked and doesn't need a manual entry here - - implementation project(':react-native-twilio-programmable-voice') + dependencies { + // override the google-service version if needed + // https://developers.google.com/android/guides/google-services-plugin + classpath 'com.google.gms:google-services:4.3.3' + } } // this plugin looks for google-services.json in your project apply plugin: 'com.google.gms.google-services' ``` -In your `AndroidManifest.xml` +#### `AndroidManifest.xml` ```xml - ..... - - - .... - +``` - ..... +If you can't or don't want to use autolink, you can also manually link the library using the instructions below (click on the arrow to show them): -``` +
+Manually link the library on Android -In `android/settings.gradle` (not necessary if auto-linking on RN 0.60+) +Make the following changes: -```gradle -... +#### `android/settings.gradle` +```groovy include ':react-native-twilio-programmable-voice' -project(':react-native-twilio-programmable-voice').projectDir = file('../node_modules/react-native-twilio-programmable-voice/android') +project(':react-native-twilio-programmable-voice').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-twilio-programmable-voice/android') +``` + +#### `android/app/build.gradle` + +```groovy +dependencies { + implementation project(':react-native-twilio-programmable-voice') +} ``` -Register module (in `MainApplication.java`) (not necessary if auto-linking on RN 0.60+ unless you want to control microphone permission) +#### `android/app/src/main/.../MainApplication.java` +On top, where imports are: ```java import com.hoxfon.react.RNTwilioVoice.TwilioVoicePackage; // <--- Import Package +``` -public class MainApplication extends Application implements ReactApplication { - - private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { - @Override - protected boolean getUseDeveloperSupport() { - return BuildConfig.DEBUG; - } +Add the `TwilioVoicePackage` class to your list of exported packages. - @Override - protected List getPackages() { - return Arrays.asList( - new MainReactPackage(), - new TwilioVoicePackage() // <---- Add the package - // new TwilioVoicePackage(false) // <---- pass false if you don't want to ask for microphone permissions - ); - } - }; - .... +```java +@Override +protected List getPackages() { + return Arrays.asList( + new MainReactPackage(), + new TwilioVoicePackage() // <---- Add the package + // new TwilioVoicePackage(false) // <---- pass false if you don't want to ask for microphone permissions + ); } ``` +
## Usage @@ -233,9 +218,11 @@ async function initTelephony() { console.err(err) } } - // iOS Only -function initTelephonyWithUrl(url) { - TwilioVoice.initWithTokenUrl(url) + +function initTelephonyWithToken(token) { + TwilioVoice.initWithAccessToken(token) + + // iOS only, configure CallKit try { TwilioVoice.configureCallKit({ appName: 'TwilioVoiceExample', // Required param @@ -263,16 +250,24 @@ TwilioVoice.addEventListener('deviceNotReady', function(data) { TwilioVoice.addEventListener('connectionDidConnect', function(data) { // { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', + // call_state: 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // } }) TwilioVoice.addEventListener('connectionIsReconnecting', function(data) { - // empty data + // { + // call_sid: string, // Twilio call sid + // call_from: string, // "+441234567890" + // call_to: string, // "client:bob" + // } }) TwilioVoice.addEventListener('connectionDidReconnect', function(data) { - // empty data + // { + // call_sid: string, // Twilio call sid + // call_from: string, // "+441234567890" + // call_to: string, // "client:bob" + // } }) TwilioVoice.addEventListener('connectionDidDisconnect', function(data: mixed) { // | null @@ -281,7 +276,7 @@ TwilioVoice.addEventListener('connectionDidDisconnect', function(data: mixed) { // } // | { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', + // call_state: 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // err?: string, @@ -290,7 +285,7 @@ TwilioVoice.addEventListener('connectionDidDisconnect', function(data: mixed) { TwilioVoice.addEventListener('callStateRinging', function(data: mixed) { // { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', + // call_state: 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // } @@ -306,7 +301,6 @@ TwilioVoice.addEventListener('callInviteCancelled', function(data: mixed) { // iOS Only TwilioVoice.addEventListener('callRejected', function(value: 'callRejected') {}) -// Android Only TwilioVoice.addEventListener('deviceDidReceiveIncoming', function(data) { // { // call_sid: string, // Twilio call sid @@ -314,12 +308,14 @@ TwilioVoice.addEventListener('deviceDidReceiveIncoming', function(data) { // call_to: string, // "client:bob" // } }) + // Android Only TwilioVoice.addEventListener('proximity', function(data) { // { // isNear: boolean // } }) + // Android Only TwilioVoice.addEventListener('wiredHeadset', function(data) { // { @@ -376,20 +372,26 @@ TwilioVoice.getCallInvite() TwilioVoice.unregister() ``` -## Twilio Voice SDK reference +## Help wanted -[iOS changelog](https://www.twilio.com/docs/api/voice-sdk/ios/changelog) +There is no need to ask permissions to contribute. Just open an issue or provide a PR. Everybody is welcome to contribute. + +ReactNative success is directly linked to its module ecosystem. One way to make an impact is helping contributing to this module or another of the many community lead ones. + +![help wanted](images/vjeux_tweet.png "help wanted") -[Android changelog](https://www.twilio.com/docs/api/voice-sdk/android/changelog) +## Twilio Voice SDK reference + +[iOS changelog](https://www.twilio.com/docs/voice/voip-sdk/ios/changelog) +[Android changelog](https://www.twilio.com/docs/voice/voip-sdk/android/3x-changelog) ## Credits [voice-quickstart-android](https://github.com/twilio/voice-quickstart-android) -[react-native-push-notification](https://github.com/zo0r/react-native-push-notification) - -[voice-quickstart-objc](https://github.com/twilio/voice-quickstart-objc) +[voice-quickstart-ios](https://github.com/twilio/voice-quickstart-ios) +[react-native-push-notification](https://github.com/zo0r/react-native-push-notification) ## License From 23b8632f60ee4183e4728d46759a6fbb4b938522 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sun, 16 Feb 2020 17:13:55 +0000 Subject: [PATCH 19/19] doc: update CHANGELOG --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 135314d0..e075a8be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - Android X migration - use gradle 5.4.1 - use API 28 - - upgrade com.twilio:voice-android to 3.3.0 + - upgrade com.twilio:voice-android to 4.3.0 - implement `hold` to hold a call - new event `callInviteCancelled` - new event `callStateRinging` @@ -16,6 +16,14 @@ - implement call ringing Twilio event - remove `call_state` from CallInvite - iOS + - implement new autolinking react native API + - update Twilio Voice SDK to v5.2.0 + - remove method `initWithAccessTokenUrl`, please use `initWithAccessToken` instead + - event parity with Android `deviceDidReceiveIncoming` + - new event `callInviteCancelled` + - new event `callStateRinging` + - new event `connectionIsReconnecting` + - new event `connectionDidReconnect` - convert params for `connectionDidConnect` to => `call_to`, from => `call_from` - convert params for `connectionDidDisconnect` to => `call_to`, from => `call_from`, `error` => `err`