From f364a3a3d23575123b941da0d2ac3ef8a43dffdb Mon Sep 17 00:00:00 2001 From: Emil Marinov Date: Tue, 26 Oct 2021 16:34:45 -0600 Subject: [PATCH 1/6] firebase pn support --- README.md | 134 ++++++++++++++++++ .../fcm/VoiceFirebaseMessagingService.java | 21 ++- 2 files changed, 151 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9e30eb8e..3b3066a4 100644 --- a/README.md +++ b/README.md @@ -569,6 +569,138 @@ TwilioVoice.getCallInvite() TwilioVoice.unregister() ``` + + + +## Support multiple Firebase-Messaging-Services in android + +1. add dependency com.google.firebase:firebase-messaging + +```gradle + dependencies { + //...other configuration here... + implementation 'com.google.firebase:firebase-messaging:19.0.+' +} +``` + + + +2. Add a new file if you don't have one (`android/app/src/main/java/com/YOUR_APP/MainMessagingService.java`) + + ```java +package com.YOUR_APP; +import io.invertase.firebase.messaging.*; +import android.content.Intent; +import android.content.Context; +import io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService; +import com.google.firebase.messaging.FirebaseMessagingService; +import com.google.firebase.messaging.RemoteMessage; +import com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService; +import com.hoxfon.react.RNTwilioVoice.fcm.VoiceFirebaseMessagingService; +import android.util.Log; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class MainMessagingService extends ReactNativeFirebaseMessagingService { + private static final String TAG = "MainMessagingService"; + + private List messagingServices = new ArrayList<>(2); + + + + public MainMessagingService() { + messagingServices.add(new RNPushNotificationListenerService(this)); + messagingServices.add(new VoiceFirebaseMessagingService(this)); + + } + + @Override + public void onNewToken(String token) { + Log.d(TAG, "onNewToken()"); + delegate(service -> service.onNewToken(token)); + } + + @Override + public void onMessageReceived(RemoteMessage remoteMessage) { + + delegate(service -> { + injectContext(service); + service.onMessageReceived(remoteMessage); + }); + } + private void injectContext(FirebaseMessagingService service) { + // tested on emulator with Android Q (Dev Preview 1) + // Accessing hidden field Landroid/content/ContextWrapper;->mBase:Landroid/content/Context; (greylist, reflection) + // + // https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces + // https://android.googlesource.com/platform/frameworks/base/+/pie-release/config/hiddenapi-light-greylist.txt + setField(service, "mBase", this); + } + + private void delegate(GCAction1 action) { + for (FirebaseMessagingService service : messagingServices) { + Log.d(TAG, " message received"+service.getPackageName()); + action.run(service); + } + } + + + private boolean setField(Object targetObject, String fieldName, Object fieldValue) { + Field field; + try { + field = targetObject.getClass().getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + field = null; + } + Class superClass = targetObject.getClass().getSuperclass(); + while (field == null && superClass != null) { + try { + field = superClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + superClass = superClass.getSuperclass(); + } + } + if (field == null) { + return false; + } + field.setAccessible(true); + try { + field.set(targetObject, fieldValue); + return true; + } catch (IllegalAccessException e) { + return false; + } + } + + interface GCAction1 { + void run(T t); + } +} + ``` + +3. Then add the following code to `android/app/src/main/AndroidManifest.xml`: + +```xml + + + + + + +``` + + * make sure you have only one service intent with action com.google.firebase.MESSAGING_EVENT + + + + ## Help wanted There is no need to ask permissions to contribute. Just open an issue or provide a PR. Everybody is welcome to contribute. @@ -593,3 +725,5 @@ ReactNative success is directly linked to its module ecosystem. One way to make ## License MIT + + 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..3645d089 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,9 @@ public void onCallInvite(final CallInvite callInvite) { Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { public void run() { - CallNotificationManager callNotificationManager = new CallNotificationManager(); + // 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 From e2603cda807071eae83ed7e9d7b1805acfac4f49 Mon Sep 17 00:00:00 2001 From: Khalidalansi Date: Tue, 7 Jun 2022 23:03:47 +0300 Subject: [PATCH 2/6] Fixed app crash when receiving voip push notification in background ios --- README.md | 338 +++------------ RNTwilioVoice.podspec | 4 +- index.js | 1 + ios/RNTwilioVoice/RNTwilioVoice.h | 5 +- ios/RNTwilioVoice/RNTwilioVoice.m | 662 +++++++++++++++++------------- 5 files changed, 434 insertions(+), 576 deletions(-) diff --git a/README.md b/README.md index 3b3066a4..3bb9d42d 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. @@ -346,8 +229,7 @@ apply plugin: 'com.google.gms.google-services' + android:name="com.hoxfon.react.RNTwilioVoice.fcm.VoiceFirebaseMessagingService"> @@ -506,6 +388,12 @@ TwilioVoice.addEventListener('deviceDidReceiveIncoming', function(data) { // } }) +TwilioVoice.addEventListener('iosCallHistoryTap', function(data) { + // { + // call_to: string, // "+441234567890" + // } +}) + // Android Only TwilioVoice.addEventListener('proximity', function(data) { // { @@ -565,142 +453,10 @@ TwilioVoice.getCallInvite() } }) -// Unregister device with Twilio +// Unregister device with Twilio (iOS only) TwilioVoice.unregister() ``` - - - -## Support multiple Firebase-Messaging-Services in android - -1. add dependency com.google.firebase:firebase-messaging - -```gradle - dependencies { - //...other configuration here... - implementation 'com.google.firebase:firebase-messaging:19.0.+' -} -``` - - - -2. Add a new file if you don't have one (`android/app/src/main/java/com/YOUR_APP/MainMessagingService.java`) - - ```java -package com.YOUR_APP; -import io.invertase.firebase.messaging.*; -import android.content.Intent; -import android.content.Context; -import io.invertase.firebase.messaging.ReactNativeFirebaseMessagingService; -import com.google.firebase.messaging.FirebaseMessagingService; -import com.google.firebase.messaging.RemoteMessage; -import com.dieam.reactnativepushnotification.modules.RNPushNotificationListenerService; -import com.hoxfon.react.RNTwilioVoice.fcm.VoiceFirebaseMessagingService; -import android.util.Log; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class MainMessagingService extends ReactNativeFirebaseMessagingService { - private static final String TAG = "MainMessagingService"; - - private List messagingServices = new ArrayList<>(2); - - - - public MainMessagingService() { - messagingServices.add(new RNPushNotificationListenerService(this)); - messagingServices.add(new VoiceFirebaseMessagingService(this)); - - } - - @Override - public void onNewToken(String token) { - Log.d(TAG, "onNewToken()"); - delegate(service -> service.onNewToken(token)); - } - - @Override - public void onMessageReceived(RemoteMessage remoteMessage) { - - delegate(service -> { - injectContext(service); - service.onMessageReceived(remoteMessage); - }); - } - private void injectContext(FirebaseMessagingService service) { - // tested on emulator with Android Q (Dev Preview 1) - // Accessing hidden field Landroid/content/ContextWrapper;->mBase:Landroid/content/Context; (greylist, reflection) - // - // https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces - // https://android.googlesource.com/platform/frameworks/base/+/pie-release/config/hiddenapi-light-greylist.txt - setField(service, "mBase", this); - } - - private void delegate(GCAction1 action) { - for (FirebaseMessagingService service : messagingServices) { - Log.d(TAG, " message received"+service.getPackageName()); - action.run(service); - } - } - - - private boolean setField(Object targetObject, String fieldName, Object fieldValue) { - Field field; - try { - field = targetObject.getClass().getDeclaredField(fieldName); - } catch (NoSuchFieldException e) { - field = null; - } - Class superClass = targetObject.getClass().getSuperclass(); - while (field == null && superClass != null) { - try { - field = superClass.getDeclaredField(fieldName); - } catch (NoSuchFieldException e) { - superClass = superClass.getSuperclass(); - } - } - if (field == null) { - return false; - } - field.setAccessible(true); - try { - field.set(targetObject, fieldValue); - return true; - } catch (IllegalAccessException e) { - return false; - } - } - - interface GCAction1 { - void run(T t); - } -} - ``` - -3. Then add the following code to `android/app/src/main/AndroidManifest.xml`: - -```xml - - - - - - -``` - - * make sure you have only one service intent with action com.google.firebase.MESSAGING_EVENT - - - - ## Help wanted There is no need to ask permissions to contribute. Just open an issue or provide a PR. Everybody is welcome to contribute. @@ -725,5 +481,3 @@ ReactNative success is directly linked to its module ecosystem. One way to make ## License MIT - - 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/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 From 6b43a1fb575b08be0b2d7bd5492d90a4b33d03b3 Mon Sep 17 00:00:00 2001 From: Khalidalansi Date: Thu, 4 Aug 2022 21:23:33 +0300 Subject: [PATCH 3/6] fix crash app --- .../CallNotificationManager.java | 24 +++++++++-- .../IncomingCallNotificationService.java | 41 ++++++++++++++----- 2 files changed, 51 insertions(+), 14 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 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..2304964d 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; @@ -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,7 @@ 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 @@ -142,12 +147,26 @@ private Spannable getActionText(Context context, @StringRes int stringRes, @Colo } private PendingIntent createActionPendingIntent(Context context, Intent intent) { - return PendingIntent.getService( - context, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT - ); + Log.d(TAG, "createActionPendingIntent"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); + stackBuilder.addNextIntentWithParentStack(intent); + // Get the PendingIntent containing the entire back stack + PendingIntent resultPendingIntent = + stackBuilder.getPendingIntent(0, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + return resultPendingIntent; + + }else{ + return PendingIntent.getService( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT | + PendingIntent.FLAG_IMMUTABLE + ); + } + } /** @@ -240,7 +259,9 @@ private String createChannel(int channelImportance) { } private void accept(CallInvite callInvite, int notificationId) { + Log.d(TAG, "accept()1"); 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); @@ -284,7 +305,7 @@ private void setCallInProgressNotification(CallInvite callInvite, int notificati int importance = NotificationManager.IMPORTANCE_LOW; if (!isAppVisible()) { if (BuildConfig.DEBUG) { - Log.i(TAG, "app is NOT visible."); + Log.i(TAG, "app is NOT visible eeeee."); } importance = NotificationManager.IMPORTANCE_HIGH; } From b2c223e032c76df912b8e8a2aa7fac10196405d3 Mon Sep 17 00:00:00 2001 From: Khalidalansi Date: Thu, 4 Aug 2022 21:49:57 +0300 Subject: [PATCH 4/6] fix crash app 2 --- .../IncomingCallNotificationService.java | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) 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 2304964d..61b226e2 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java @@ -148,25 +148,31 @@ private Spannable getActionText(Context context, @StringRes int stringRes, @Colo private PendingIntent createActionPendingIntent(Context context, Intent intent) { Log.d(TAG, "createActionPendingIntent"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); - stackBuilder.addNextIntentWithParentStack(intent); - // Get the PendingIntent containing the entire back stack - PendingIntent resultPendingIntent = - stackBuilder.getPendingIntent(0, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - return resultPendingIntent; - - }else{ - return PendingIntent.getService( - context, - 0, - intent, - PendingIntent.FLAG_UPDATE_CURRENT | - PendingIntent.FLAG_IMMUTABLE - ); - } - + // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); + // stackBuilder.addNextIntentWithParentStack(intent); + // // Get the PendingIntent containing the entire back stack + // PendingIntent resultPendingIntent = + // stackBuilder.getPendingIntent(0, + // PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + // return resultPendingIntent; + + // }else{ + // return PendingIntent.getService( + // context, + // 0, + // intent, + // PendingIntent.FLAG_UPDATE_CURRENT | + // PendingIntent.FLAG_IMMUTABLE + // ); + // } + return PendingIntent.getService( + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT | + PendingIntent.FLAG_IMMUTABLE + ); } /** From 07bba106d5292e154c7539a45cb34cd33182987d Mon Sep 17 00:00:00 2001 From: Khalidalansi Date: Tue, 9 Aug 2022 20:43:35 +0300 Subject: [PATCH 5/6] remove lines `not needed` --- .../IncomingCallNotificationService.java | 23 ++----------------- .../fcm/VoiceFirebaseMessagingService.java | 1 - 2 files changed, 2 insertions(+), 22 deletions(-) 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 61b226e2..ce28b878 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java @@ -147,25 +147,6 @@ private Spannable getActionText(Context context, @StringRes int stringRes, @Colo } private PendingIntent createActionPendingIntent(Context context, Intent intent) { - Log.d(TAG, "createActionPendingIntent"); - // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - // TaskStackBuilder stackBuilder = TaskStackBuilder.create(this); - // stackBuilder.addNextIntentWithParentStack(intent); - // // Get the PendingIntent containing the entire back stack - // PendingIntent resultPendingIntent = - // stackBuilder.getPendingIntent(0, - // PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); - // return resultPendingIntent; - - // }else{ - // return PendingIntent.getService( - // context, - // 0, - // intent, - // PendingIntent.FLAG_UPDATE_CURRENT | - // PendingIntent.FLAG_IMMUTABLE - // ); - // } return PendingIntent.getService( context, 0, @@ -265,7 +246,7 @@ private String createChannel(int channelImportance) { } private void accept(CallInvite callInvite, int notificationId) { - Log.d(TAG, "accept()1"); + endForeground(); Log.d(TAG, "accept()"); Intent activeCallIntent = new Intent(this, getMainActivityClass(this)); @@ -311,7 +292,7 @@ private void setCallInProgressNotification(CallInvite callInvite, int notificati int importance = NotificationManager.IMPORTANCE_LOW; if (!isAppVisible()) { if (BuildConfig.DEBUG) { - Log.i(TAG, "app is NOT visible eeeee."); + Log.i(TAG, "app is NOT visible."); } importance = NotificationManager.IMPORTANCE_HIGH; } 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 3645d089..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 @@ -86,7 +86,6 @@ 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)serviceRef.getApplication()).getReactNativeHost().getReactInstanceManager(); ReactContext context = mReactInstanceManager.getCurrentReactContext(); From 1a6a9b49ba0db119ff79d8249db581a49810d50d Mon Sep 17 00:00:00 2001 From: Khalidalansi Date: Thu, 25 Aug 2022 02:48:49 +0300 Subject: [PATCH 6/6] change getService to getActivity for issues Notification trampoline restrictions --- README.md | 10 +- .../IncomingCallNotificationService.java | 97 +++++++++++-------- .../RNTwilioVoice/TwilioVoiceModule.java | 20 +++- 3 files changed, 83 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 3bb9d42d..059a6f3e 100644 --- a/README.md +++ b/README.md @@ -224,7 +224,15 @@ apply plugin: 'com.google.gms.google-services' ```xml - + + + + + + + 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 ce28b878..19a49746 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java @@ -67,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; @@ -101,7 +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 = createPendingIntentGetActivity(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 @@ -128,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(); } @@ -140,20 +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_IMMUTABLE - ); + context, + 0, + intent, + PendingIntent.FLAG_UPDATE_CURRENT | + PendingIntent.FLAG_IMMUTABLE); } /** @@ -172,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(); @@ -231,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); @@ -246,7 +259,6 @@ private String createChannel(int channelImportance) { } private void accept(CallInvite callInvite, int notificationId) { - endForeground(); Log.d(TAG, "accept()"); Intent activeCallIntent = new Intent(this, getMainActivityClass(this)); @@ -258,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);