diff --git a/README.md b/README.md
index 9e30eb8e..059a6f3e 100644
--- a/README.md
+++ b/README.md
@@ -16,13 +16,12 @@ Tested with:
The most updated branch is [feat/twilio-android-sdk-5](https://github.com/hoxfon/react-native-twilio-programmable-voice/tree/feat/twilio-android-sdk-5) which is aligned with:
-- Android 5.4.2
+- Android 5.0.2
- iOS 5.2.0
It contains breaking changes from `react-native-twilio-programmable-voice` v4, and it will be released as v5.
You can install it with:
-
```bash
# Yarn
yarn add https://github.com/hoxfon/react-native-twilio-programmable-voice#feat/twilio-android-sdk-5
@@ -54,159 +53,6 @@ Allow Android to use the built in Android telephony service to make and receive
- Android 4.5.0
- iOS 5.2.0
-### Breaking changes in v5.0.0
-
-Changes on [Android Twilio Voice SDK v5](https://www.twilio.com/docs/voice/voip-sdk/android/3x-changelog#500) are reflected in the JavaScript API, the way call invites are handled has changed and other v5 features like `audioSwitch` have been implemented.
-`setSpeakerPhone()` has been removed from Android, use selectAudioDevice(name: string) instead.
-
-#### Background incoming calls
-
-- When the app is not in foreground incoming calls result in a heads-up notification with action to "ACCEPT" and "REJECT".
-- ReactMethod `accept` does not dispatch any event. In v4 it dispatched `connectionDidDisconnect`.
-- ReactMethod `reject` dispatches a `callInviteCancelled` event instead of `connectionDidDisconnect`.
-- ReactMethod `ignore` does not dispatch any event. In v4 it dispatched `connectionDidDisconnect`.
-
-To show heads up notifications, you must add the following lines to your application's `android/app/src/main/AndroidManifest.xml`:
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-Firebase Messaging 19.0.+ is imported by this module, so there is no need to import it in your app's `bundle.gradle` file.
-
-In v4 the flow to launch the app when receiving a call was:
-
-1. the module launched the app
-2. after the React app is initialised, it always asked to the native module whether there were incoming call invites
-3. if there were any incoming call invites, the module would have sent an event to the React app with the incoming call invite parameters
-4. the Reach app would have listened to the event and would have launched the view with the appropriate incoming call answer/reject controls
-
-This loop was long and prone to race conditions. For example,when the event was sent before the React main view was completely initialised, it would not be handled at all.
-
-V5 replaces the previous flow by using `getLaunchOptions()` to pass initial properties from the native module to React, when receiving a call invite as explained here: https://reactnative.dev/docs/communication-android.
-
-The React app is launched with the initial properties `callInvite` or `call`.
-
-To handle correctly `lauchedOptions`, you must add the following blocks to your app's `MainActivity`:
-
-```java
-
-import com.hoxfon.react.RNTwilioVoice.TwilioModule;
-...
-
-public class MainActivity extends ReactActivity {
-
- @Override
- protected ReactActivityDelegate createReactActivityDelegate() {
- return new ReactActivityDelegate(this, getMainComponentName()) {
- @Override
- protected ReactRootView createRootView() {
- return new RNGestureHandlerEnabledRootView(MainActivity.this);
- }
- @Override
- protected Bundle getLaunchOptions() {
- return TwilioModule.getActivityLaunchOption(this.getPlainActivity().getIntent());
- }
- };
- }
-
- // ...
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
- setShowWhenLocked(true);
- setTurnScreenOn(true);
- }
- getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
- | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
- | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
- }
-
- // ...
-}
-```
-
-#### Audio Switch
-
-Access to native Twilio SDK AudioSwitch module for Android has been added to the JavaScript API:
-
-```javascript
-// getAudioDevices returns all audio devices connected
-// {
-// "Speakerphone": false,
-// "Earnpiece": true, // true indicates the selected device
-// }
-getAudioDevices()
-
-// getSelectedAudioDevice returns the selected audio device
-getSelectedAudioDevice()
-
-// selectAudioDevice selects the passed audio device for the current active call
-selectAudioDevice(name: string)
-```
-
-#### Event deviceDidReceiveIncoming
-
-When a call invite is received, the [SHAKEN/STIR](https://www.twilio.com/docs/voice/trusted-calling-using-shakenstir) `caller_verification` field has been added to the list of params for `deviceDidReceiveIncoming`. Values are: `verified`, `unverified`, `unknown`.
-
-## ICE
-
-See https://www.twilio.com/docs/stun-turn
-
-```bash
-curl -X POST https://api.twilio.com/2010-04-01/Accounts/ACb0b56ae3bf07ce4045620249c3c90b40/Tokens.json \
--u ACb0b56ae3bf07ce4045620249c3c90b40:f5c84f06e5c02b55fa61696244a17c84
-```
-
-```java
-Set iceServers = new HashSet<>();
-// server URLs returned by calling the Twilio Rest API to generate a new token
-iceServers.add(new IceServer("stun:global.stun.twilio.com:3478?transport=udp"));
-iceServers.add(new IceServer("turn:global.turn.twilio.com:3478?transport=udp","8e6467be547b969ad913f7bdcfb73e411b35f648bd19f2c1cb4161b4d4a067be","n8zwmkgjIOphHN93L/aQxnkUp1xJwrZVLKc/RXL0ZpM="));
-iceServers.add(new IceServer("turn:global.turn.twilio.com:3478?transport=tcp","8e6467be547b969ad913f7bdcfb73e411b35f648bd19f2c1cb4161b4d4a067be","n8zwmkgjIOphHN93L/aQxnkUp1xJwrZVLKc/RXL0ZpM="));
-iceServers.add(new IceServer("turn:global.turn.twilio.com:443?transport=tcp","8e6467be547b969ad913f7bdcfb73e411b35f648bd19f2c1cb4161b4d4a067be","n8zwmkgjIOphHN93L/aQxnkUp1xJwrZVLKc/RXL0ZpM="));
-
-IceOptions iceOptions = new IceOptions.Builder()
- .iceServers(iceServers)
- .build();
-
-ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken)
- .iceOptions(iceOptions)
- .enableDscp(true)
- .params(twiMLParams)
- .build();
-```
-
### Breaking changes in v4.0.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.
@@ -311,6 +157,43 @@ To pass caller's name to CallKit via Voip push notification add custom parameter
```
+Your app must initialize PKPushRegistry with PushKit push type VoIP at the launch time. As mentioned in the
+[PushKit guidelines](https://developer.apple.com/documentation/pushkit/supporting_pushkit_notifications_in_your_app),
+the system can't deliver push notifications to your app until you create a PKPushRegistry object for VoIP push type and set the delegate. If your app delays the initialization of PKPushRegistry, your app may receive outdated
+PushKit push notifications, and if your app decides not to report the received outdated push notifications to CallKit, iOS may terminate your app.
+
+We will initialize push kit only if RN code had called TwilioVoice.initWithAccessToken(token) and we've cached device token. You can pass same arguments to initPushKitIfTokenCached as you would pass to configureCallKit
+
+```obj-c
+// add import
+#import
+
+@implementation AppDelegate { // <-- add bracket and next two lines
+ RCTBridge* bridge;
+}
+
+- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
+{
+ bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions]; // REMOVE RCTBridge*
+ // ...
+
+ // add these two lines
+ _voice = [bridge moduleForClass:RNTwilioVoice.class];
+ [_voice initPushKitIfTokenCached:@{ @"appName" : @"YOUR FANCY APP NAME" }];
+
+ return YES;
+}
+
+// add this method to handle taps in call log
+- (BOOL)application:(UIApplication *)application
+continueUserActivity:(NSUserActivity *)userActivity
+ restorationHandler:(void(^)(NSArray> *restorableObjects))restorationHandler {
+ RNTwilioVoice* _voice = [bridge moduleForClass:RNTwilioVoice.class];
+ [_voice handleRestoration:userActivity];
+ return YES;
+}
+```
+
#### 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.
@@ -341,13 +224,20 @@ apply plugin: 'com.google.gms.google-services'
```xml
-
+
+
+
+
+
+
+
+ android:name="com.hoxfon.react.RNTwilioVoice.fcm.VoiceFirebaseMessagingService">
@@ -506,6 +396,12 @@ TwilioVoice.addEventListener('deviceDidReceiveIncoming', function(data) {
// }
})
+TwilioVoice.addEventListener('iosCallHistoryTap', function(data) {
+ // {
+ // call_to: string, // "+441234567890"
+ // }
+})
+
// Android Only
TwilioVoice.addEventListener('proximity', function(data) {
// {
@@ -565,7 +461,7 @@ TwilioVoice.getCallInvite()
}
})
-// Unregister device with Twilio
+// Unregister device with Twilio (iOS only)
TwilioVoice.unregister()
```
diff --git a/RNTwilioVoice.podspec b/RNTwilioVoice.podspec
index 398bedf8..97089cf8 100644
--- a/RNTwilioVoice.podspec
+++ b/RNTwilioVoice.podspec
@@ -16,9 +16,9 @@ Pod::Spec.new do |s|
s.source = { git: 'https://github.com/hoxfon/react-native-twilio-programmable-voice', tag: s.version }
s.dependency 'React-Core'
- s.dependency 'TwilioVoice', '~> 5.2.0'
+ s.dependency 'TwilioVoice', '~> 6.3.0'
s.xcconfig = { 'FRAMEWORK_SEARCH_PATHS' => '${PODS_ROOT}/TwilioVoice/Build/iOS' }
s.frameworks = 'TwilioVoice'
s.preserve_paths = 'LICENSE', 'README.md', 'package.json', 'index.js'
-end
+end
\ 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 0e5a3951..a18cd06f 100644
--- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java
+++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java
@@ -52,6 +52,22 @@ public int getApplicationImportance(ReactApplicationContext context) {
}
return 0;
}
+ public static PendingIntent createPendingIntentGetActivity(Context context, int id, Intent intent, int flag) {
+ Log.d(TAG, "createPendingIntentGetActivity");
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ return PendingIntent.getActivity(context, id, intent, PendingIntent.FLAG_IMMUTABLE | flag);
+ } else {
+ return PendingIntent.getActivity(context, id, intent, flag);
+ }
+ }
+
+ public static PendingIntent createPendingIntentGetBroadCast(Context context, int id, Intent intent, int flag) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ return PendingIntent.getBroadcast(context, id, intent, PendingIntent.FLAG_IMMUTABLE | flag);
+ } else {
+ return PendingIntent.getBroadcast(context, id, intent, flag);
+ }
+ }
public static Class getMainActivityClass(Context context) {
String packageName = context.getPackageName();
@@ -77,7 +93,7 @@ public void createMissedCallNotification(ReactApplicationContext context, String
.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, Constants.MISSED_CALLS_NOTIFICATION_ID)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- PendingIntent pendingIntent = PendingIntent.getActivity(
+ PendingIntent pendingIntent = createPendingIntentGetActivity(
context,
0,
intent,
@@ -89,7 +105,7 @@ public void createMissedCallNotification(ReactApplicationContext context, String
0,
new Intent(Constants.ACTION_CLEAR_MISSED_CALLS_COUNT)
.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, Constants.CLEAR_MISSED_CALLS_NOTIFICATION_ID),
- 0
+ 0|PendingIntent.FLAG_IMMUTABLE
);
/*
* Pass the notification id and call sid to use as an identifier to open the notification
@@ -151,7 +167,7 @@ public void createHangupNotification(ReactApplicationContext context, String cal
.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, Constants.HANGUP_NOTIFICATION_ID)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- PendingIntent pendingIntent = PendingIntent.getActivity(
+ PendingIntent pendingIntent = createPendingIntentGetActivity(
context,
0,
intent,
@@ -163,7 +179,7 @@ public void createHangupNotification(ReactApplicationContext context, String cal
0,
new Intent(Constants.ACTION_HANGUP_CALL)
.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, Constants.HANGUP_NOTIFICATION_ID),
- PendingIntent.FLAG_UPDATE_CURRENT
+ PendingIntent.FLAG_IMMUTABLE
);
Bundle extras = new Bundle();
diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java
index c78175c0..19a49746 100644
--- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java
+++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java
@@ -6,6 +6,7 @@
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
+import android.app.TaskStackBuilder;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
@@ -30,6 +31,7 @@
import com.twilio.voice.CallInvite;
+import static com.hoxfon.react.RNTwilioVoice.CallNotificationManager.createPendingIntentGetActivity;
import static com.hoxfon.react.RNTwilioVoice.CallNotificationManager.getMainActivityClass;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG;
@@ -65,11 +67,11 @@ public int onStartCommand(Intent intent, int flags, int startId) {
case Constants.ACTION_JS_ANSWER:
endForeground();
- break;
+ break;
case Constants.ACTION_JS_REJECT:
endForeground();
- break;
+ break;
default:
break;
@@ -84,9 +86,13 @@ public IBinder onBind(Intent intent) {
private Notification createNotification(CallInvite callInvite, int notificationId, int channelImportance) {
Context context = getApplicationContext();
-
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "createNotification");
+ }
Intent intent = new Intent(this, getMainActivityClass(context));
+
intent.setAction(Constants.ACTION_INCOMING_CALL_NOTIFICATION);
+
intent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);
intent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite);
intent.putExtra(Constants.CALL_SID, callInvite.getCallSid());
@@ -95,8 +101,8 @@ private Notification createNotification(CallInvite callInvite, int notificationI
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- PendingIntent pendingIntent =
- PendingIntent.getActivity(this, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent pendingIntent = createPendingIntentGetActivity(this, notificationId, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
/*
* Pass the notification id and call sid to use as an identifier to cancel the
@@ -123,6 +129,7 @@ private Notification createNotification(CallInvite callInvite, int notificationI
.setExtras(extras)
.setContentIntent(pendingIntent)
.setGroup("test_app_notification")
+ .setCategory(Notification.CATEGORY_CALL)
.setColor(Color.rgb(214, 10, 37))
.build();
}
@@ -135,19 +142,25 @@ private Spannable getActionText(Context context, @StringRes int stringRes, @Colo
new ForegroundColorSpan(context.getColor(colorRes)),
0,
spannable.length(),
- 0
- );
+ 0);
}
return spannable;
}
- private PendingIntent createActionPendingIntent(Context context, Intent intent) {
+ private PendingIntent createActionPendingIntent(Context context, Intent intent,int notificationId) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ return PendingIntent.getActivity(
+ context,
+ notificationId,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE);
+ }
return PendingIntent.getService(
context,
0,
intent,
- PendingIntent.FLAG_UPDATE_CURRENT
- );
+ PendingIntent.FLAG_UPDATE_CURRENT |
+ PendingIntent.FLAG_IMMUTABLE);
}
/**
@@ -166,41 +179,46 @@ private Notification buildNotification(String text,
int notificationId,
String channelId) {
Context context = getApplicationContext();
-
- Intent rejectIntent = new Intent(context, IncomingCallNotificationService.class);
+ Intent rejectIntent = null;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ rejectIntent = new Intent(context, getMainActivityClass(context));
+ }else{
+ rejectIntent = new Intent(context, IncomingCallNotificationService.class);
+ }
rejectIntent.setAction(Constants.ACTION_REJECT);
rejectIntent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite);
rejectIntent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);
NotificationCompat.Action rejectAction = new NotificationCompat.Action.Builder(
android.R.drawable.ic_menu_delete,
getActionText(context, R.string.reject, R.color.red),
- createActionPendingIntent(context, rejectIntent)
- ).build();
-
- Intent acceptIntent = new Intent(context, IncomingCallNotificationService.class);
+ createActionPendingIntent(context, rejectIntent,notificationId)).build();
+ Intent acceptIntent = null;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ acceptIntent = new Intent(context, getMainActivityClass(context));
+ }else{
+ acceptIntent = new Intent(context, IncomingCallNotificationService.class);
+ }
acceptIntent.setAction(Constants.ACTION_ACCEPT);
acceptIntent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite);
acceptIntent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);
+ acceptIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
NotificationCompat.Action answerAction = new NotificationCompat.Action.Builder(
android.R.drawable.ic_menu_call,
getActionText(context, R.string.accept, R.color.green),
- createActionPendingIntent(context, acceptIntent)
- ).build();
-
- NotificationCompat.Builder builder =
- new NotificationCompat.Builder(context, channelId)
- .setSmallIcon(R.drawable.ic_call_white_24dp)
- .setContentTitle(getString(R.string.call_incoming_title))
- .setContentText(text)
- .setExtras(extras)
- .setAutoCancel(true)
- .addAction(rejectAction)
- .addAction(answerAction)
- .setFullScreenIntent(pendingIntent, true)
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setCategory(Notification.CATEGORY_CALL)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- ;
+ createActionPendingIntent(context, acceptIntent,notificationId)).build();
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
+ .setSmallIcon(R.drawable.ic_call_white_24dp)
+ .setContentTitle(getString(R.string.call_incoming_title))
+ .setContentText(text)
+ .setExtras(extras)
+ .setAutoCancel(true)
+ .addAction(rejectAction)
+ .addAction(answerAction)
+ .setFullScreenIntent(pendingIntent, true)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(Notification.CATEGORY_CALL)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
// build notification large icon
Resources res = context.getResources();
@@ -225,12 +243,13 @@ private String createChannel(int channelImportance) {
callInviteChannel.setLightColor(Color.GREEN);
// TODO set sound for background incoming call
-// Uri defaultRingtoneUri = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_RINGTONE);
-// AudioAttributes audioAttributes = new AudioAttributes.Builder()
-// .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
-// .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
-// .build();
-// callInviteChannel.setSound(defaultRingtoneUri, audioAttributes);
+ // Uri defaultRingtoneUri = RingtoneManager.getActualDefaultRingtoneUri(this,
+ // RingtoneManager.TYPE_RINGTONE);
+ // AudioAttributes audioAttributes = new AudioAttributes.Builder()
+ // .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ // .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+ // .build();
+ // callInviteChannel.setSound(defaultRingtoneUri, audioAttributes);
callInviteChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
@@ -241,6 +260,7 @@ private String createChannel(int channelImportance) {
private void accept(CallInvite callInvite, int notificationId) {
endForeground();
+ Log.d(TAG, "accept()");
Intent activeCallIntent = new Intent(this, getMainActivityClass(this));
activeCallIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
activeCallIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -250,6 +270,7 @@ private void accept(CallInvite callInvite, int notificationId) {
activeCallIntent.putExtra(Constants.CALL_FROM, callInvite.getFrom());
activeCallIntent.putExtra(Constants.CALL_TO, callInvite.getTo());
activeCallIntent.setAction(Constants.ACTION_ACCEPT);
+
this.startActivity(activeCallIntent);
}
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 cab0d85d..5c3d6cff 100644
--- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java
+++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java
@@ -3,6 +3,7 @@
import android.Manifest;
import android.app.Activity;
import android.app.ActivityManager.RunningAppProcessInfo;
+import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -63,6 +64,7 @@
import java.util.Map;
import java.util.List;
+import static com.hoxfon.react.RNTwilioVoice.CallNotificationManager.getMainActivityClass;
import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CONNECTION_DID_CONNECT;
import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CONNECTION_DID_DISCONNECT;
import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_DEVICE_DID_RECEIVE_INCOMING;
@@ -729,8 +731,24 @@ public void acceptFromIntent(Intent intent) {
.enableDscp(true)
.build();
activeCallInvite.accept(getReactApplicationContext(), acceptOptions, callListener);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ removeAcceptNotification(intent,getReactApplicationContext());
+ getReactApplicationContext().stopService(new Intent(getReactApplicationContext(), IncomingCallNotificationService.class));
+ }
+
+ }
+
+ public void removeAcceptNotification(Intent intent,ReactApplicationContext context) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "removeAcceptNotification()");
+ }
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ int notificationId = intent.getIntExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, 0);
+ notificationManager.cancel(notificationId);
}
+
@ReactMethod
public void accept() {
SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging();
@@ -741,7 +759,7 @@ public void accept() {
if (BuildConfig.DEBUG) {
Log.d(TAG, "accept()");
}
-
+ Log.d(TAG, "ACTION_JS_ANSWER()");
Intent intent = new Intent(getReactApplicationContext(), IncomingCallNotificationService.class);
intent.setAction(Constants.ACTION_JS_ANSWER);
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 15dd4395..1a8da668 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
@@ -33,6 +33,19 @@
public class VoiceFirebaseMessagingService extends FirebaseMessagingService {
+ private CallNotificationManager callNotificationManager;
+ private FirebaseMessagingService mFirebaseServiceDelegate;
+
+ public VoiceFirebaseMessagingService() {
+ super();
+ }
+ public VoiceFirebaseMessagingService(FirebaseMessagingService delegate) {
+ super();
+ this.mFirebaseServiceDelegate = delegate;
+ callNotificationManager = new CallNotificationManager();
+
+ }
+
@Override
public void onCreate() {
super.onCreate();
@@ -59,12 +72,12 @@ public void onMessageReceived(RemoteMessage remoteMessage) {
// Check if message contains a data payload.
if (remoteMessage.getData().size() > 0) {
Map data = remoteMessage.getData();
-
+ final FirebaseMessagingService serviceRef = (this.mFirebaseServiceDelegate == null) ? this : this.mFirebaseServiceDelegate;
// If notification ID is not provided by the user for push notification, generate one at random
Random randomNumberGenerator = new Random(System.currentTimeMillis());
final int notificationId = randomNumberGenerator.nextInt();
- boolean valid = Voice.handleMessage(this, data, new MessageListener() {
+ boolean valid = Voice.handleMessage(serviceRef, data, new MessageListener() {
@Override
public void onCallInvite(final CallInvite callInvite) {
// We need to run this on the main thread, as the React code assumes that is true.
@@ -73,9 +86,8 @@ public void onCallInvite(final CallInvite callInvite) {
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
public void run() {
- CallNotificationManager callNotificationManager = new CallNotificationManager();
// Construct and load our normal React JS code bundle
- ReactInstanceManager mReactInstanceManager = ((ReactApplication) getApplication()).getReactNativeHost().getReactInstanceManager();
+ ReactInstanceManager mReactInstanceManager = ((ReactApplication)serviceRef.getApplication()).getReactNativeHost().getReactInstanceManager();
ReactContext context = mReactInstanceManager.getCurrentReactContext();
// initialise appImportance to the highest possible importance in case context is null
diff --git a/index.js b/index.js
index e472949b..6ef624b1 100644
--- a/index.js
+++ b/index.js
@@ -23,6 +23,7 @@ const _eventHandlers = {
callInviteCancelled: new Map(),
callRejected: new Map(),
audioDevicesUpdated: new Map(),
+ iosCallHistoryTap: new Map(),
}
const Twilio = {
diff --git a/ios/RNTwilioVoice/RNTwilioVoice.h b/ios/RNTwilioVoice/RNTwilioVoice.h
index 3de40ae8..a8d945d0 100644
--- a/ios/RNTwilioVoice/RNTwilioVoice.h
+++ b/ios/RNTwilioVoice/RNTwilioVoice.h
@@ -3,4 +3,7 @@
@interface RNTwilioVoice : RCTEventEmitter
-@end
+- (void) initPushKitIfTokenCached: (NSDictionary *)callKitParams;
+- (BOOL) handleRestoration: (NSUserActivity *)userActivity;
+
+@end
\ No newline at end of file
diff --git a/ios/RNTwilioVoice/RNTwilioVoice.m b/ios/RNTwilioVoice/RNTwilioVoice.m
index 1c8a96b9..ba23bbea 100644
--- a/ios/RNTwilioVoice/RNTwilioVoice.m
+++ b/ios/RNTwilioVoice/RNTwilioVoice.m
@@ -5,14 +5,21 @@
@import PushKit;
@import CallKit;
@import TwilioVoice;
+@import Intents;
+
NSString * const kCachedDeviceToken = @"CachedDeviceToken";
+NSString * const kCachedTokenUrl = @"CachedTokenUrl";
+NSString * const kCachedBindingTime = @"CachedBindingTime";
NSString * const kCallerNameCustomParameter = @"CallerName";
+static NSInteger const kRegistrationTTLInDays = 365;
+
@interface RNTwilioVoice ()
@property (nonatomic, strong) PKPushRegistry *voipRegistry;
@property (nonatomic, strong) void(^incomingPushCompletionCallback)(void);
+@property (nonatomic, strong) TVOCallInvite *callInvite;
@property (nonatomic, strong) void(^callKitCompletionCallback)(BOOL);
@property (nonatomic, strong) TVODefaultAudioDevice *audioDevice;
@property (nonatomic, strong) NSMutableDictionary *activeCallInvites;
@@ -27,10 +34,12 @@ @interface RNTwilioVoice () *)supportedEvents
{
- return @[@"connectionDidConnect", @"connectionDidDisconnect", @"callRejected", @"deviceReady", @"deviceNotReady", @"deviceDidReceiveIncoming", @"callInviteCancelled", @"callStateRinging", @"connectionIsReconnecting", @"connectionDidReconnect"];
+ return @[
+ @"connectionDidConnect",
+ @"connectionDidDisconnect",
+ @"callRejected",
+ @"deviceReady",
+ @"deviceNotReady",
+ @"deviceDidReceiveIncoming",
+ @"callInviteCancelled",
+ @"callStateRinging",
+ @"connectionIsReconnecting",
+ @"connectionDidReconnect",
+ @"iosCallHistoryTap"
+ ];
}
@synthesize bridge = _bridge;
- (void)dealloc {
- if (self.callKitProvider) {
- [self.callKitProvider invalidate];
- }
+ if (self.callKitProvider) {
+ [self.callKitProvider invalidate];
+ }
+
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+/*
+ We need to init push kit immediately at start. But it might be first start of the app
+ so in that case lets pass initialization to RN code
+ */
+- (void) initPushKitIfTokenCached: (NSDictionary *)callKitParams {
+ _deregisterQueued = false;
+ NSData *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken];
+ if (cachedDeviceToken && [cachedDeviceToken length] > 0) {
+ [self initPushRegistry];
+ [self configureCallKit:callKitParams];
+ }
+}
+
+- (BOOL) handleRestoration: (NSUserActivity *)userActivity {
+ INStartAudioCallIntent *callIntent = (INStartAudioCallIntent *)userActivity.interaction.intent;
+ if (callIntent.contacts[0]) {
+ INPersonHandle *handle = callIntent.contacts[0].personHandle;
+ if ([handle.value length] > 0) {
+ // Start a new call with CallKit
+ NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init];
+ [callParams setObject:callIntent.contacts[0].personHandle.value forKey:@"call_to"];
+ [self sendEventWithName:@"iosCallHistoryTap" body:callParams];
+ }
+ }
- [[NSNotificationCenter defaultCenter] removeObserver:self];
+ return YES;
}
RCT_EXPORT_METHOD(initWithAccessToken:(NSString *)token) {
- _token = token;
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAppTerminateNotification) name:UIApplicationWillTerminateNotification object:nil];
- [self initPushRegistry];
+ _token = token;
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAppTerminateNotification) name:UIApplicationWillTerminateNotification object:nil];
+ [self initPushRegistry];
+ // if push kit is in init state and new device token waits to be registered/deregistered with our access token
+ [self registerNewDeviceToken];
+ [self deregisterDeviceToken];
}
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;
- configuration.maximumCallsPerCallGroup = 1;
- if (_settings[@"imageName"]) {
- configuration.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:_settings[@"imageName"]]);
- }
- if (_settings[@"ringtoneSound"]) {
- configuration.ringtoneSound = _settings[@"ringtoneSound"];
- }
+ 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];
+ TwilioVoiceSDK.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;
+ configuration.maximumCallsPerCallGroup = 1;
+ if (_settings[@"imageName"]) {
+ configuration.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:_settings[@"imageName"]]);
+ }
+ if (_settings[@"ringtoneSound"]) {
+ configuration.ringtoneSound = _settings[@"ringtoneSound"];
+ }
+ configuration.supportedHandleTypes = [NSSet setWithArray:@[@(CXHandleTypeGeneric), @(CXHandleTypePhoneNumber)]];
- _callKitProvider = [[CXProvider alloc] initWithConfiguration:configuration];
- [_callKitProvider setDelegate:self queue:nil];
+ _callKitProvider = [[CXProvider alloc] initWithConfiguration:configuration];
+ [_callKitProvider setDelegate:self queue:nil];
- NSLog(@"CallKit Initialized");
+ NSLog(@"CallKit Initialized");
- self.callKitCallController = [[CXCallController alloc] init];
- }
+ self.callKitCallController = [[CXCallController alloc] init];
+ }
}
RCT_EXPORT_METHOD(connect: (NSDictionary *)params) {
- NSLog(@"Calling phone number %@", [params valueForKey:@"To"]);
+ NSLog(@"Calling phone number %@", [params valueForKey:@"To"]);
- UIDevice* device = [UIDevice currentDevice];
- device.proximityMonitoringEnabled = YES;
+ UIDevice* device = [UIDevice currentDevice];
+ device.proximityMonitoringEnabled = YES;
- if (self.activeCall && self.activeCall.state == TVOCallStateConnected) {
- [self performEndCallActionWithUUID:self.activeCall.uuid];
- } else {
- NSUUID *uuid = [NSUUID UUID];
- NSString *handle = [params valueForKey:@"To"];
- _callParams = [[NSMutableDictionary alloc] initWithDictionary:params];
- [self performStartCallActionWithUUID:uuid handle:handle];
- }
+ if (self.activeCall && self.activeCall.state == TVOCallStateConnected) {
+ [self performEndCallActionWithUUID:self.activeCall.uuid];
+ } else {
+ NSUUID *uuid = [NSUUID UUID];
+ NSString *handle = [params valueForKey:@"To"];
+ _callParams = [[NSMutableDictionary alloc] initWithDictionary:params];
+ [self performStartCallActionWithUUID:uuid handle:handle];
+ }
}
RCT_EXPORT_METHOD(disconnect) {
@@ -122,12 +175,12 @@ - (void)dealloc {
}
RCT_EXPORT_METHOD(setMuted: (BOOL *)muted) {
- NSLog(@"Mute/UnMute call");
+ NSLog(@"Mute/UnMute call");
self.activeCall.muted = muted ? YES : NO;
}
RCT_EXPORT_METHOD(setOnHold: (BOOL *)isOnHold) {
- NSLog(@"Hold/Unhold call");
+ NSLog(@"Hold/Unhold call");
self.activeCall.onHold = isOnHold ? YES : NO;
}
@@ -136,28 +189,29 @@ - (void)dealloc {
}
RCT_EXPORT_METHOD(sendDigits: (NSString *)digits) {
- if (self.activeCall && self.activeCall.state == TVOCallStateConnected) {
- NSLog(@"SendDigits %@", digits);
- [self.activeCall sendDigits:digits];
- }
+ if (self.activeCall && self.activeCall.state == TVOCallStateConnected) {
+ NSLog(@"SendDigits %@", digits);
+ [self.activeCall sendDigits:digits];
+ }
}
RCT_EXPORT_METHOD(unregister) {
- NSLog(@"unregister");
- NSString *accessToken = [self fetchAccessToken];
- 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 {
- [[NSUserDefaults standardUserDefaults] setValue:@"" forKey:kCachedDeviceToken];
- NSLog(@"Successfully unregistered for VoIP push notifications.");
- }
- }];
- }
+ NSLog(@"unregister");
+ NSData *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken];
+ if ([cachedDeviceToken length] > 0) {
+ NSString *accessToken = [self fetchAccessToken];
+ [TwilioVoiceSDK 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.");
+ }
+ }];
+ // lets remove cached device token so we can register with new twilio access token (e.g. after login with another user)
+ [[NSUserDefaults standardUserDefaults] removeObjectForKey:kCachedDeviceToken];
+ }
}
RCT_REMAP_METHOD(getActiveCall,
@@ -206,98 +260,140 @@ - (void)dealloc {
}
- (void)initPushRegistry {
- self.voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
- self.voipRegistry.delegate = self;
- self.voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
+ self.voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
+ self.voipRegistry.delegate = self;
+ self.voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
}
- (NSString *)fetchAccessToken {
- if (_tokenUrl) {
- NSString *accessToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:_tokenUrl]
- encoding:NSUTF8StringEncoding
- error:nil];
- return accessToken;
- } else {
- return _token;
- }
+ if (_tokenUrl) {
+ NSString *accessToken = [NSString stringWithContentsOfURL:[NSURL URLWithString:_tokenUrl]
+ encoding:NSUTF8StringEncoding
+ error:nil];
+ return accessToken;
+ } else {
+ return _token;
+ }
}
-#pragma mark - PKPushRegistryDelegate
-- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type {
- NSLog(@"pushRegistry:didUpdatePushCredentials:forType");
-
- if ([type isEqualToString:PKPushTypeVoIP]) {
- const unsigned *tokenBytes = [credentials.token bytes];
- 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];
- NSString *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken];
- if (![cachedDeviceToken isEqualToString:deviceTokenString]) {
- cachedDeviceToken = deviceTokenString;
+/**
+ * The TTL of a registration is 1 year. The TTL for registration for this device/identity pair is reset to
+ * 1 year whenever a new registration occurs or a push notification is sent to this device/identity pair.
+ * This method checks if binding exists in UserDefaults, and if half of TTL has been passed then the method
+ * will return true, else false.
+ */
+- (BOOL)registrationRequired {
+ BOOL registrationRequired = YES;
+ NSDate *lastBindingCreated = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedBindingTime];
- /*
- * Perform registration if a new device token is detected.
- */
- [TwilioVoice registerWithAccessToken:accessToken
- deviceToken:cachedDeviceToken
- completion:^(NSError *error) {
+ if (lastBindingCreated) {
+ NSDateComponents *dayComponent = [[NSDateComponents alloc] init];
+
+ // Register upon half of the TTL
+ dayComponent.day = kRegistrationTTLInDays / 2;
+
+ NSDate *bindingExpirationDate = [[NSCalendar currentCalendar] dateByAddingComponents:dayComponent toDate:lastBindingCreated options:0];
+ NSDate *currentDate = [NSDate date];
+ if ([bindingExpirationDate compare:currentDate] == NSOrderedDescending) {
+ registrationRequired = NO;
+ }
+ }
+ return registrationRequired;
+}
+
+- (void) registerNewDeviceToken {
+ if (!_newDeviceToken || !_token) {
+ return;
+ }
+
+ NSData *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken];
+ if ([self registrationRequired] || ![cachedDeviceToken isEqualToData:_newDeviceToken]) {
+ cachedDeviceToken = _newDeviceToken;
+ NSString *accessToken = [self fetchAccessToken];
+
+ [TwilioVoiceSDK 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 {
+ } else {
NSLog(@"Successfully registered for VoIP push notifications.");
- /*
- * Save the device token after successfully registered.
- */
+ // Save the device token after successfully registered.
[[NSUserDefaults standardUserDefaults] setObject:cachedDeviceToken forKey:kCachedDeviceToken];
+
+ /**
+ * The TTL of a registration is 1 year. The TTL for registration for this device/identity
+ * pair is reset to 1 year whenever a new registration occurs or a push notification is
+ * sent to this device/identity pair.
+ */
+ [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:kCachedBindingTime];
[self sendEventWithName:@"deviceReady" body:nil];
}
- }];
+ }];
}
- }
+ _newDeviceToken = NULL;
}
-- (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type {
- NSLog(@"pushRegistry:didInvalidatePushTokenForType");
+- (void) deregisterDeviceToken {
+ if (!_deregisterQueued || !_token) {
+ return;
+ }
- if ([type isEqualToString:PKPushTypeVoIP]) {
NSString *accessToken = [self fetchAccessToken];
- NSString *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken];
+ NSData *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 {
- [[NSUserDefaults standardUserDefaults] setValue:@"" forKey:kCachedDeviceToken];
- NSLog(@"Successfully unregistered for VoIP push notifications.");
- }
- }];
+ [TwilioVoiceSDK 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.");
+ }
+ }];
+ }
+ _deregisterQueued = false;
+}
+
+#pragma mark - PKPushRegistryDelegate
+- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)credentials forType:(NSString *)type {
+ NSLog(@"pushRegistry:didUpdatePushCredentials:forType");
+
+ if ([type isEqualToString:PKPushTypeVoIP]) {
+ // we might get updated credentials before RN code calls initWithAccessToken so we cannot register new credentials
+ _newDeviceToken = credentials.token;
+ [self registerNewDeviceToken];
+ }
+}
+
+- (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(PKPushType)type {
+ NSLog(@"pushRegistry:didInvalidatePushTokenForType");
+
+ if ([type isEqualToString:PKPushTypeVoIP]) {
+ // we might get updated credentials before RN code calls initWithAccessToken so we cannot register new credentials
+ _deregisterQueued = true;
+ [self deregisterDeviceToken];
}
- }
}
/**
-* 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.
-*/
+ * 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]) {
- // 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.");
- }
- }
+ NSLog(@"pushRegistry:didReceiveIncomingPushWithPayload:forType");
+ if ([type isEqualToString:PKPushTypeVoIP]) {
+ // The Voice SDK will use main queue to invoke `cancelledCallInviteReceived:error` when delegate queue is not passed
+ if (![TwilioVoiceSDK handleNotification:payload.dictionaryPayload delegate:self delegateQueue: nil]) {
+ NSLog(@"This is not a valid Twilio Voice notification.");
+ }
+ }
}
/**
@@ -316,7 +412,7 @@ - (void)pushRegistry:(PKPushRegistry *)registry
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]) {
+ if (![TwilioVoiceSDK handleNotification:payload.dictionaryPayload delegate:self delegateQueue: nil]) {
NSLog(@"This is not a valid Twilio Voice notification.");
}
}
@@ -325,9 +421,9 @@ - (void)pushRegistry:(PKPushRegistry *)registry
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.
- */
+ * 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();
}
}
@@ -346,15 +442,16 @@ - (void)callInviteReceived:(TVOCallInvite *)callInvite {
* provide you a `TVOCallInvite` object. Report the incoming call to CallKit upon receiving this callback.
*/
NSLog(@"callInviteReceived");
+ NSString *callerCustomName = NULL;
NSString *from = @"Unknown";
if (callInvite.from) {
from = [callInvite.from stringByReplacingOccurrencesOfString:@"client:" withString:@""];
}
if (callInvite.customParameters[kCallerNameCustomParameter]) {
- from = callInvite.customParameters[kCallerNameCustomParameter];
+ callerCustomName = callInvite.customParameters[kCallerNameCustomParameter];
}
// Always report to CallKit
- [self reportIncomingCallFrom:from withUUID:callInvite.uuid];
+ [self reportIncomingCallFrom:from withUUID:callInvite.uuid withCallerCustomName:callerCustomName];
self.activeCallInvites[[callInvite.uuid UUIDString]] = callInvite;
if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) {
[self incomingPushHandled];
@@ -362,23 +459,23 @@ - (void)callInviteReceived:(TVOCallInvite *)callInvite {
NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
if (callInvite.callSid) {
- [params setObject:callInvite.callSid forKey:@"call_sid"];
+ [params setObject:callInvite.callSid forKey:@"call_sid"];
}
if (callInvite.from) {
- [params setObject:callInvite.from forKey:@"call_from"];
+ [params setObject:callInvite.from forKey:@"call_from"];
}
if (callInvite.to) {
- [params setObject:callInvite.to forKey:@"call_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.
- */
+ * 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) {
@@ -407,11 +504,11 @@ - (void)cancelledCallInviteReceived:(nonnull TVOCancelledCallInvite *)cancelledC
- (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 %@", 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];
@@ -437,7 +534,7 @@ - (void)cancelledCallInviteReceived:(TVOCancelledCallInvite *)cancelledCallInvit
}
- (void)notificationError:(NSError *)error {
- NSLog(@"notificationError: %@", [error localizedDescription]);
+ NSLog(@"notificationError: %@", [error localizedDescription]);
}
#pragma mark - TVOCallDelegate
@@ -460,24 +557,24 @@ - (void)callDidStartRinging:(TVOCall *)call {
#pragma mark - TVOCallDelegate
- (void)callDidConnect:(TVOCall *)call {
- NSLog(@"callDidConnect");
- self.callKitCompletionCallback(YES);
-
- NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init];
- [callParams setObject:call.sid forKey:@"call_sid"];
- if (call.state == TVOCallStateConnecting) {
- [callParams setObject:StateConnecting forKey:@"call_state"];
- } else if (call.state == TVOCallStateConnected) {
- [callParams setObject:StateConnected forKey:@"call_state"];
- }
-
- if (call.from) {
- [callParams setObject:call.from forKey:@"call_from"];
- }
- if (call.to) {
- [callParams setObject:call.to forKey:@"call_to"];
- }
- [self sendEventWithName:@"connectionDidConnect" body:callParams];
+ NSLog(@"callDidConnect");
+ self.callKitCompletionCallback(YES);
+
+ NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init];
+ [callParams setObject:call.sid forKey:@"call_sid"];
+ if (call.state == TVOCallStateConnecting) {
+ [callParams setObject:StateConnecting forKey:@"call_state"];
+ } else if (call.state == TVOCallStateConnected) {
+ [callParams setObject:StateConnected forKey:@"call_state"];
+ }
+
+ if (call.from) {
+ [callParams setObject:call.from forKey:@"call_from"];
+ }
+ if (call.to) {
+ [callParams setObject:call.to forKey:@"call_to"];
+ }
+ [self sendEventWithName:@"connectionDidConnect" body:callParams];
}
- (void)call:(TVOCall *)call isReconnectingWithError:(NSError *)error {
@@ -485,10 +582,10 @@ - (void)call:(TVOCall *)call isReconnectingWithError:(NSError *)error {
NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init];
[callParams setObject:call.sid forKey:@"call_sid"];
if (call.from) {
- [callParams setObject:call.from forKey:@"call_from"];
+ [callParams setObject:call.from forKey:@"call_from"];
}
if (call.to) {
- [callParams setObject:call.to forKey:@"call_to"];
+ [callParams setObject:call.to forKey:@"call_to"];
}
[self sendEventWithName:@"connectionIsReconnecting" body:callParams];
}
@@ -498,18 +595,18 @@ - (void)callDidReconnect:(TVOCall *)call {
NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init];
[callParams setObject:call.sid forKey:@"call_sid"];
if (call.from) {
- [callParams setObject:call.from forKey:@"call_from"];
+ [callParams setObject:call.from forKey:@"call_from"];
}
if (call.to) {
- [callParams setObject:call.to forKey:@"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);
+ NSLog(@"Call failed to connect: %@", error);
- self.callKitCompletionCallback(NO);
+ self.callKitCompletionCallback(NO);
[self performEndCallActionWithUUID:call.uuid];
[self callDisconnected:call error:error];
}
@@ -547,7 +644,7 @@ - (void)callDisconnected:(TVOCall *)call error:(NSError *)error {
if (error) {
NSString* errMsg = [error localizedDescription];
if (error.localizedFailureReason) {
- errMsg = [error localizedFailureReason];
+ errMsg = [error localizedFailureReason];
}
[params setObject:errMsg forKey:@"err"];
}
@@ -592,66 +689,66 @@ - (void)toggleAudioRoute:(BOOL)toSpeaker {
#pragma mark - CXProviderDelegate
- (void)providerDidReset:(CXProvider *)provider {
- NSLog(@"providerDidReset");
+ NSLog(@"providerDidReset");
self.audioDevice.enabled = YES;
}
- (void)providerDidBegin:(CXProvider *)provider {
- NSLog(@"providerDidBegin");
+ NSLog(@"providerDidBegin");
}
- (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession {
- NSLog(@"provider:didActivateAudioSession");
+ NSLog(@"provider:didActivateAudioSession");
self.audioDevice.enabled = YES;
}
- (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession {
- NSLog(@"provider:didDeactivateAudioSession");
+ NSLog(@"provider:didDeactivateAudioSession");
self.audioDevice.enabled = NO;
}
- (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action {
- NSLog(@"provider:timedOutPerformingAction");
+ NSLog(@"provider:timedOutPerformingAction");
}
- (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action {
- NSLog(@"provider:performStartCallAction");
+ NSLog(@"provider:performStartCallAction");
self.audioDevice.enabled = NO;
self.audioDevice.block();
- [self.callKitProvider reportOutgoingCallWithUUID:action.callUUID startedConnectingAtDate:[NSDate date]];
+ [self.callKitProvider reportOutgoingCallWithUUID:action.callUUID startedConnectingAtDate:[NSDate date]];
- __weak typeof(self) weakSelf = self;
- [self performVoiceCallWithUUID:action.callUUID client:nil completion:^(BOOL success) {
- __strong typeof(self) strongSelf = weakSelf;
- if (success) {
- [strongSelf.callKitProvider reportOutgoingCallWithUUID:action.callUUID connectedAtDate:[NSDate date]];
- [action fulfill];
- } else {
- [action fail];
- }
- }];
+ __weak typeof(self) weakSelf = self;
+ [self performVoiceCallWithUUID:action.callUUID client:nil completion:^(BOOL success) {
+ __strong typeof(self) strongSelf = weakSelf;
+ if (success) {
+ [strongSelf.callKitProvider reportOutgoingCallWithUUID:action.callUUID connectedAtDate:[NSDate date]];
+ [action fulfill];
+ } else {
+ [action fail];
+ }
+ }];
}
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action {
- NSLog(@"provider:performAnswerCallAction");
+ NSLog(@"provider:performAnswerCallAction");
- self.audioDevice.enabled = NO;
- self.audioDevice.block();
- [self performAnswerVoiceCallWithUUID:action.callUUID completion:^(BOOL success) {
- if (success) {
- [action fulfill];
- } else {
- [action fail];
- }
- }];
+ self.audioDevice.enabled = NO;
+ self.audioDevice.block();
+ [self performAnswerVoiceCallWithUUID:action.callUUID completion:^(BOOL success) {
+ if (success) {
+ [action fulfill];
+ } else {
+ [action fail];
+ }
+ }];
- [action fulfill];
+ [action fulfill];
}
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action {
- NSLog(@"provider:performEndCallAction");
+ NSLog(@"provider:performEndCallAction");
TVOCallInvite *callInvite = self.activeCallInvites[action.callUUID.UUIDString];
TVOCall *call = self.activeCalls[action.callUUID.UUIDString];
@@ -667,17 +764,17 @@ - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)
}
self.audioDevice.enabled = YES;
- [action fulfill];
+ [action fulfill];
}
- (void)provider:(CXProvider *)provider performSetHeldCallAction:(CXSetHeldCallAction *)action {
TVOCall *call = self.activeCalls[action.callUUID.UUIDString];
- if (call) {
- [call setOnHold:action.isOnHold];
- [action fulfill];
- } else {
- [action fail];
- }
+ if (call) {
+ [call setOnHold:action.isOnHold];
+ [action fulfill];
+ } else {
+ [action fail];
+ }
}
- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action {
@@ -691,92 +788,95 @@ - (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCal
}
- (void)provider:(CXProvider *)provider performPlayDTMFCallAction:(CXPlayDTMFCallAction *)action {
- TVOCall *call = self.activeCalls[action.callUUID.UUIDString];
- if (call && call.state == TVOCallStateConnected) {
- NSLog(@"SendDigits %@", action.digits);
- [call sendDigits:action.digits];
- }
+ TVOCall *call = self.activeCalls[action.callUUID.UUIDString];
+ if (call && call.state == TVOCallStateConnected) {
+ NSLog(@"SendDigits %@", action.digits);
+ [call sendDigits:action.digits];
+ }
}
#pragma mark - CallKit Actions
- (void)performStartCallActionWithUUID:(NSUUID *)uuid handle:(NSString *)handle {
- if (uuid == nil || handle == nil) {
- return;
- }
+ if (uuid == nil || handle == nil) {
+ return;
+ }
- CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
- CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle];
- CXTransaction *transaction = [[CXTransaction alloc] initWithAction:startCallAction];
+ CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:handle];
+ CXStartCallAction *startCallAction = [[CXStartCallAction alloc] initWithCallUUID:uuid handle:callHandle];
+ CXTransaction *transaction = [[CXTransaction alloc] initWithAction:startCallAction];
- [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) {
- if (error) {
- NSLog(@"StartCallAction transaction request failed: %@", [error localizedDescription]);
- } else {
- NSLog(@"StartCallAction transaction request successful");
+ [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) {
+ if (error) {
+ NSLog(@"StartCallAction transaction request failed: %@", [error localizedDescription]);
+ } else {
+ NSLog(@"StartCallAction transaction request successful");
- CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
- callUpdate.remoteHandle = callHandle;
- callUpdate.supportsDTMF = YES;
- callUpdate.supportsHolding = YES;
- callUpdate.supportsGrouping = NO;
- callUpdate.supportsUngrouping = NO;
- callUpdate.hasVideo = NO;
+ CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
+ callUpdate.remoteHandle = callHandle;
+ callUpdate.supportsDTMF = YES;
+ callUpdate.supportsHolding = YES;
+ callUpdate.supportsGrouping = NO;
+ callUpdate.supportsUngrouping = NO;
+ callUpdate.hasVideo = NO;
- [self.callKitProvider reportCallWithUUID:uuid updated:callUpdate];
- }
- }];
+ [self.callKitProvider reportCallWithUUID:uuid updated:callUpdate];
+ }
+ }];
}
-- (void)reportIncomingCallFrom:(NSString *)from withUUID:(NSUUID *)uuid {
- CXHandleType type = [[from substringToIndex:1] isEqual:@"+"] ? CXHandleTypePhoneNumber : CXHandleTypeGeneric;
- // lets replace 'client:' with ''
- CXHandle *callHandle = [[CXHandle alloc] initWithType:type value:[from stringByReplacingOccurrencesOfString:@"client:" withString:@""]];
+- (void)reportIncomingCallFrom:(NSString *)from withUUID:(NSUUID *)uuid withCallerCustomName:(NSString *)name {
+ CXHandleType type = [[from substringToIndex:1] isEqual:@"+"] ? CXHandleTypePhoneNumber : CXHandleTypeGeneric;
+ // lets replace 'client:' with ''
+ CXHandle *callHandle = [[CXHandle alloc] initWithType:type value:[from stringByReplacingOccurrencesOfString:@"client:" withString:@""]];
- CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
- callUpdate.remoteHandle = callHandle;
- callUpdate.supportsDTMF = YES;
- callUpdate.supportsHolding = YES;
- callUpdate.supportsGrouping = NO;
- callUpdate.supportsUngrouping = NO;
- callUpdate.hasVideo = NO;
-
- [self.callKitProvider reportNewIncomingCallWithUUID:uuid update:callUpdate completion:^(NSError *error) {
- if (!error) {
- NSLog(@"Incoming call successfully reported");
- } else {
- NSLog(@"Failed to report incoming call successfully: %@.", [error localizedDescription]);
+ CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
+ if (name) {
+ callUpdate.localizedCallerName = name;
}
- }];
+ callUpdate.remoteHandle = callHandle;
+ callUpdate.supportsDTMF = YES;
+ callUpdate.supportsHolding = YES;
+ callUpdate.supportsGrouping = NO;
+ callUpdate.supportsUngrouping = NO;
+ callUpdate.hasVideo = NO;
+
+ [self.callKitProvider reportNewIncomingCallWithUUID:uuid update:callUpdate completion:^(NSError *error) {
+ if (!error) {
+ NSLog(@"Incoming call successfully reported");
+ } else {
+ NSLog(@"Failed to report incoming call successfully: %@.", [error localizedDescription]);
+ }
+ }];
}
- (void)performEndCallActionWithUUID:(NSUUID *)uuid {
- if (uuid == nil) {
- return;
- }
+ if (uuid == nil) {
+ return;
+ }
- CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:uuid];
- CXTransaction *transaction = [[CXTransaction alloc] initWithAction:endCallAction];
+ CXEndCallAction *endCallAction = [[CXEndCallAction alloc] initWithCallUUID:uuid];
+ CXTransaction *transaction = [[CXTransaction alloc] initWithAction:endCallAction];
- [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) {
- if (error) {
- NSLog(@"EndCallAction transaction request failed: %@", [error localizedDescription]);
- }
- }];
+ [self.callKitCallController requestTransaction:transaction completion:^(NSError *error) {
+ if (error) {
+ NSLog(@"EndCallAction transaction request failed: %@", [error localizedDescription]);
+ }
+ }];
}
- (void)performVoiceCallWithUUID:(NSUUID *)uuid
client:(NSString *)client
completion:(void(^)(BOOL success))completionHandler {
- __weak typeof(self) weakSelf = 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;
+ __strong typeof(self) strongSelf = weakSelf;
+ builder.params = strongSelf->_callParams;
+ builder.uuid = uuid;
}];
- TVOCall *call = [TwilioVoice connectWithOptions:connectOptions delegate:self];
+ TVOCall *call = [TwilioVoiceSDK connectWithOptions:connectOptions delegate:self];
if (call) {
- self.activeCall = call;
- self.activeCalls[call.uuid.UUIDString] = call;
+ self.activeCall = call;
+ self.activeCalls[call.uuid.UUIDString] = call;
}
self.callKitCompletionCallback = completionHandler;
}
@@ -808,12 +908,12 @@ - (void)performAnswerVoiceCallWithUUID:(NSUUID *)uuid
}
- (void)handleAppTerminateNotification {
- NSLog(@"handleAppTerminateNotification called");
+ NSLog(@"handleAppTerminateNotification called");
- if (self.activeCall) {
- NSLog(@"handleAppTerminateNotification disconnecting an active call");
- [self.activeCall disconnect];
- }
+ if (self.activeCall) {
+ NSLog(@"handleAppTerminateNotification disconnecting an active call");
+ [self.activeCall disconnect];
+ }
}
@end