diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..2674beb3 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@x-guard:registry=https://npm.pkg.github.com/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index db2b8b76..e075a8be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,74 +1,131 @@ [Release Section](https://github.com/hoxfon/react-native-twilio-programmable-voice/releases) +## 4.0.0 + +- Android + - implement new autolinking react native API + - update Firebase Messaging to 17.3.4 which simplifies how to obtain the FCM token + - Android X migration + - use gradle 5.4.1 + - use API 28 + - upgrade com.twilio:voice-android to 4.3.0 + - implement `hold` to hold a call + - new event `callInviteCancelled` + - new event `callStateRinging` + - new method `getCallInvite` + - implement call ringing Twilio event + - remove `call_state` from CallInvite +- iOS + - implement new autolinking react native API + - update Twilio Voice SDK to v5.2.0 + - remove method `initWithAccessTokenUrl`, please use `initWithAccessToken` instead + - event parity with Android `deviceDidReceiveIncoming` + - new event `callInviteCancelled` + - new event `callStateRinging` + - new event `connectionIsReconnecting` + - new event `connectionDidReconnect` + - convert params for `connectionDidConnect` to => `call_to`, from => `call_from` + - convert params for `connectionDidDisconnect` to => `call_to`, from => `call_from`, `error` => `err` + +- throw an error when listening to events that do not exist + +## 3.21.3 + +- iOS: Upgrade TwilioVoice pod to version 2.1 + +## 3.21.2 + +- Switch from rnpm to react-native.config.js + ## 3.21.1 + - Android: fix crash when asking for microphone permission before an activity is displayed ## 3.21.0 + - Android: allow to pass arbitrary parameters to call voice.call() as it is on iOS ## 3.20.1 + - iOS: fix crash when callSid is nil for CallInviteCanceled ## 3.20.0 + - Android: option to opt out microphone permission request ## 3.19.0 + - upgrade com.twilio:voice-android to 2.0.7 - upgrade firebase-messaging to 17.+ ## 3.18.1 + - Validate token type before calling native module ## 3.18.0 + - avoid keeping the screen on when a call is received with a locked device - remove PowerManager wakelock pattern ## 3.17.0 + - disconnect any existing calls when the app is terminated - Android: Twilio Voice SDK 2.0.6 - iOS: Twilio Voice SDK 2.0.4 ## 3.16.0 + - Android: Twilio Voice SDK 2.0.5 ## 3.15.0 + - init notifications channel before showing call in progress notification - add cocoapods support to install iOS native package ## 3.14.0 + - Android: start up the app in fullscreen for incoming calls - Android: pass call params to disconnect event when ignoring a call - Android: pass call params to disconnect event when rejecting a call ## 3.13.1 + - Android: Twilio Voice SDK 2.0.4 ## 3.13.0 + - iOS: the library is compatible with Twilio Voice SDK 2.0.2 ## 3.12.0 + - iOS: the library is compatible with Twilio Voice SDK 2.0.0-beta21 - iOS: handle events when a call is put on hold ## 3.11.0 + - iOS: the library is compatible with Twilio Voice SDK 2.0.0-beta20 ## 3.10.0 + - fix crash on Oreo abandonAudioFocusRequest() ## 3.9.0 + - update com.google.gms:google-services to 3.1.2 - use latest API 26 support library ## 3.8.0 + - Android: Twilio Voice SDK 2.0.2 ## 3.7.0 + - Android: use proximity sensor to lock screen during calls - Android: send event to JavaScript for headset plugged in - Android: fix unset audio focus for Android O ## 3.6.0 + - Android: Twilio Voice SDK 2.0.0-beta24 - Implement Android O notification channels - Android: ensure that audio settings are set back to normal when the app destroys @@ -76,75 +133,98 @@ - Android: prevent other apps to emit sound when a call is in progress ## 3.5.0 + - Android: Twilio Voice SDK 2.0.0-beta20 - Implement Call.Listener onConnectFailure() ## 3.4.0 + - Fix iOS HEADER_SEARCH_PATHS ## 3.3.0 + - Android: Twilio Voice SDK 2.0.0-beta18 - Adapt setAudioFocus() for Android O ## 3.2.0 + - Android: add compatibility for react native >= 0.47 ## 3.1.1 + - iOS: ensure the proximity sensor is enabled when starting a call ## 3.1.0 + Make the iOS initialization process the same as Android + - iOS: call event `deviceReady` only when the accessToken registration is successful - iOS: implement event `deviceNotReady` called when the accessToken registration is not successful ## 3.0.0 + Breaking changes: + - initWitToken returns an object with a property `initialized` instead of `initilized` - iOS event `connectionDidConnect` returns the same properties as Android -move property `to` => `call_to` -move property `from` => `call_from` + move property `to` => `call_to` + move property `from` => `call_from` New iOS + - iOS: the library is compatible with Twilio Voice SDK 2.0.0-beta15 - iOS use CallKit reportOutgoingCallWithUUID when initializing calls New Android + - add properties `call_to` and `call_from` to Android event `connectionDidConnect` ## 2.11.2 + - Make sure CallKit session is ended when the call is terminated by the callee - @SimonRobinson ## 2.11.1 + - Make sure CallKit session is ended on fail - @Pagebakers ## 2.11.0 + - Android: Twilio Voice SDK 2.0.0-beta17 ## 2.10.0 + - Android: Twilio Voice SDK 2.0.0-beta16 ## 2.9.0 + - make sure the Android build uses the latest version 10 of firebase.messaging to avoid dependencies conflicts crashes ## 2.8.0 + - iOS: prevent CallKit to be initialised more than once ## 2.7.0 + - iOS: correct handling of calls disconnection ## 2.6.0 + - iOS: implementing getActiveCall() ## 2.5.2 + - iOS: initWithToken() now returns the same value as Android ## 2.5.1 + - iOS: handle call failure and pass to JS the most descriptive error ## 2.5.0 + - iOS: Twilio Voice SDK 2.0.0-beta13 ## 2.4.0 + - Android: Twilio Voice SDK 2.0.0-beta15 - use buildToolsVersion "25.0.2" - use targetSdkVersion 25 @@ -153,71 +233,91 @@ New Android - fix ingore() not not sending CONNECTION_STOP to JavaScript when there is not activeInviteCall ## 2.3.2 + - Android: Twilio Voice SDK 2.0.0-beta14 ## 2.3.1 + - iOS: call TwilioVoice audioSessionDeactivated on didDeactivateAudioSession - iOS: performEndCallActionWithUUID when call is disconnected from the app ## 2.3.0 + - Android: Twilio Voice SDK 2.0.0-beta13 -- iOS: Twilio Voice SDK 2.0.0-beta11 +- iOS: Twilio Voice SDK 2.0.0-beta11 ## 2.2.0 + - iOS: Twilio Voice SDK 2.0.0-beta10 ## 2.1.0 + - Android: Twilio Voice SDK 2.0.0-beta11 ## 2.0.2 + - Android: fix library for RN 0.45.1 ## 2.0.1 + - ios: send connectionDidDisconnect when the call invite terminates ## 2.0.0 + - ios implementation with CallKit ## 1.1.0 + - use Twilio Voice SDK 2.0.0-beta8 ## 1.0.1 + - Android: use incoming call notification full screen ## 1.0.0 + - use Twilio beta 5 - removed requestPermissions, react-native API should be used instead - renamed getIncomingCall() to getActiveCall() - set the audio of the call as MODE_IN_COMMUNICATION ## 0.6.3 + - fix crash when activityManager is null - add call_from and call_to to the event connectionDidDisconnect ## 0.6.2 + - Android: fix. Clear callInvite when the caller hangs up ## 0.6.1 + - Android: fix gradle import beta4 ## 0.6.0 + - Android: use Twilio Voice SDK Beta4 ## 0.5.5 + - improve logic for starting the MainActivity when receiving a call. The Intent flags depends on the App importance (fixes the 0.5.3 for Android 6.0) - make sure all wakelock are released after being acquired, and the keyguard re-enabled ## 0.5.4 + - set incoming call Intent flag depending on App importance (App status) ## 0.5.3 + - Prevent incoming call from starting a new task: use (Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP) for the Intent - Prevent the incoming call Intent to be broadcast when the app is in the foreground ## 0.5.2 + - allow custom notification icons ## 0.5.1 + - fix showing incoming call - add pendingIntent to clear missed calls number - prevent double incoming call cancelled message @@ -268,7 +368,6 @@ New Android - Check if Google Play Services are available before initialising Twilio for receiving calls. - Method initWithToken returns a Promise to let the application know if the initialisation did succeed. - ## 0.2.2 - fix the instruction to setup the `AndroidManifest.xml` file diff --git a/README.md b/README.md index b0e65d07..a0b7e2b4 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,19 @@ # react-native-twilio-programmable-voice -This is a React Native wrapper for Twilio Programmable Voice SDK that lets you make and receive calls from your ReactNatvie App. This module is not curated nor maintained, but inspired by Twilio. -# Twilio Programmable Voice SDK +This is a React-Native wrapper for [Twilio Programmable Voice SDK](https://www.twilio.com/voice) which lets you make and receive calls from your React-Native App. This module is not affiliated with nor officially maintained by Twilio, and it is maintained by open source contributors. -- Android 2.0.7 (bundled within this library) -- iOS 2.0.4 (specified by the app's own podfile) +## Twilio Programmable Voice SDK -## Breaking changes in v3.0.0 - -- initWitToken returns an object with a property `initialized` instead of `initilized` -- iOS event `connectionDidConnect` returns the same properties as Android -move property `to` => `call_to` -move property `from` => `call_from` - -## Migrating Android from v1 to v2 (incoming call use FCM) - -You will need to make changes both on your Twilio account using Twilio Web Console and on your react native app. -Twilio Programmable Voice Android SDK uses `FCM` since version 2.0.0.beta5. +- Android 4.5.0 (bundled within the module) +- iOS 5.1.0 (specified by the app's own podfile) -Before you start, I strongly suggest that you read the list of Twilio changes from Android SDK v2.0.0 beta4 to beta5: -[Twilio example App: Migrating from GCM to FCM](https://github.com/twilio/voice-quickstart-android/blob/d7d4f0658e145eb94ab8f5e34f6fd17314e7ab17/README.md#migrating-from-gcm-to-fcm) +## Breaking changes in v4.0.0 -These are all the changes required: +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. -- remove all the GCM related code from your `AndroidManifest.xml` and add the following code to receive `FCM` notifications -(I wasn't successful in keeping react-native-fcm working at the same time. If you know how please open an issue to share). +Android: update Firebase Messaging to 17.6.+. Remove the following block from your application's `AndroidManifest.xml` if you are migrating from v3. ```xml - ..... - - - - - - - - - - ``` -- log into your Firebase console. Navigate to: Project settings > CLOUD MESSAGING. Copy your `Server key` -- in Twilio console add a new Push Credential, type `FCM`, fcm secret Firebase FCM `Server key` -- include in your project `google-services.json`; if you have not include it yet -- rename getIncomingCall() to getActiveCall() +Android X is supported. -If something doesn't work as expected or you want to make a request open an issue. +Data passed to the event `deviceDidReceiveIncoming` does not contain the key `call_state`, because state of Call Invites was removed in Twilio Android and iOS SDK v3.0.0 -## Help wanted! +- iOS: params changes for `connectionDidConnect` and `connectionDidDisconnect` -There is no need to ask permissions to contribute. Just open an issue or provide a PR. Everybody is welcome to contribute. + to => call_to + from => call_from + error => err -ReactNative success is directly linked to its module ecosystem. One way to make an impact is helping contributing to this module or another of the many community lead ones. +New features -![help wanted](images/vjeux_tweet.png "help wanted") +Twilio Programmable Voice SDK v3.0.0 handles call invites directly and makes it easy to distinguish a call invites from an active call, which previously was confusing. +To ensure that an active call is displayed when the app comes to foreground you should use the promise `getActiveCall()`. +To ensure that a call invite is displayed when the app comes to foreground use the promise `getCallInvite()`. Please note that call invites don't have a `call_state` field. + +You should use `hold()` to put a call on hold. + +You can be notified when a call is `ringing` by listening for `callStateRinging` events. + +iOS application can now receive the following events, that in v3 where only dispatched to Android: + +- deviceDidReceiveIncoming +- callInviteCancelled +- callStateRinging +- connectionIsReconnecting +- connectionDidReconnect + +## Breaking changes in v3.0.0 + +- initWitToken returns an object with a property `initialized` instead of `initilized` +- iOS event `connectionDidConnect` returns the same properties as Android +move property `to` => `call_to` +move property `from` => `call_from` ## Installation Before starting, we recommend you get familiar with [Twilio Programmable Voice SDK](https://www.twilio.com/docs/api/voice-sdk). It's easier to integrate this module into your react-native app if you follow the Quick start tutorial from Twilio, because it makes very clear which setup steps are required. - -``` +```bash npm install react-native-twilio-programmable-voice --save -react-native link react-native-twilio-programmable-voice ``` -### iOS Installation - when projects made with react-native init -After you have linked the library with `react-native link react-native-twilio-programmable-voice` -check that `libRNTwilioVoice.a` is present under YOUR_TARGET > Build Phases > Link Binaries With Libraries. If it is not present you can add it using the + sign at the bottom of that list. +- **React Native 0.60+** -Edit your `Podfile` to include TwilioVoice framework +[CLI autolink feature](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md) links the module while building the app. +- **React Native <= 0.59** + +```bash +react-native link react-native-twilio-programmable-voice ``` -source 'https://github.com/cocoapods/specs' -# min version for TwilioVoice to work -platform :ios, '8.1' +### iOS Installation -target do - ... - pod 'TwilioVoice', '~> 2.0.0' - ... -end +If you can't or don't want to use autolink, you can also manually link the library using the instructions below (click on the arrow to show them): -``` +
+Manually link the library on iOS + +Follow the [instructions in the React Native documentation](https://facebook.github.io/react-native/docs/linking-libraries-ios#manual-linking) to manually link the framework -run `pod install` from inside your project `ios` directory +After you have linked the library with `react-native link react-native-twilio-programmable-voice` +check that `libRNTwilioVoice.a` is present under YOUR_TARGET > Build Phases > Link Binaries With Libraries. If it is not present you can add it using the + sign at the bottom of that list. +
-### iOS Installation - when projects made without react-native init -Edit your `Podfile` to include TwilioVoice and RNTwilioVoice frameworks +Edit your `Podfile` to include TwilioVoice framework -``` +```ruby source 'https://github.com/cocoapods/specs' # min version for TwilioVoice to work -platform :ios, '8.1' +platform :ios, '10.0' target do ... - pod 'TwilioVoice', '~> 2.0.0' - pod 'RNTwilioVoice', path: '../node_modules/react-native-twilio-programmable-voice' + pod 'TwilioVoice', '~> 5.2.0' ... end - ``` -run `pod install` from inside your project `ios` directory - -### CallKit +```bash +cd ios/ && pod install +``` -The current iOS part of this library works through [CallKit](https://developer.apple.com/reference/callkit). Because of this the call flow is much simpler than on Android as CallKit handles the inbound calls answering, ignoring, or rejecting. -Because of CallKit, the only event listeners present are "deviceReady", "connectionDidConnect", "connectionDidDisconnect", and "callRejected". +#### CallKit -### VoIP Service Certificate +The iOS library works through [CallKit](https://developer.apple.com/reference/callkit) and handling calls is much simpler than the Android implementation as CallKit handles the inbound calls answering, ignoring, or rejecting. Outbound calls must be controlled by custom React-Native screens and controls. -Twilio Programmable Voice for iOS utilizes Apple's VoIP Services and VoIP "Push Notifications" instead of FCM. You will need a VoIP Service Certificate from Apple to receive calls. +#### VoIP Service Certificate +Twilio Programmable Voice for iOS utilizes Apple's VoIP Services and VoIP "Push Notifications" instead of FCM. You will need a VoIP Service Certificate from Apple to receive calls. Follow [the official Twilio instructions](https://github.com/twilio/voice-quickstart-ios#7-create-voip-service-certificate) to complete this step. ## Android Installation @@ -135,41 +126,27 @@ Setup FCM You must download the file `google-services.json` from the Firebase console. It contains keys and settings for all your applications under Firebase. This library obtains the resource `senderID` for registering for remote GCM from that file. -**NOTE: To use a specific `play-service-gcm` version, update the `compile` instruction in your App's `android/app/build.gradle` (replace `10.+` with the version you prefer):** - -```gradle -... +#### `android/build.gradle` +```groovy buildscript { - ... - dependencies { - classpath 'com.google.gms:google-services:3.1.2' - } -} - -... - -dependencies { - ... - - compile project(':react-native-twilio-programmable-voice') + dependencies { + // override the google-service version if needed + // https://developers.google.com/android/guides/google-services-plugin + classpath 'com.google.gms:google-services:4.3.3' + } } // this plugin looks for google-services.json in your project apply plugin: 'com.google.gms.google-services' ``` -In your `AndroidManifest.xml` +#### `AndroidManifest.xml` ```xml - ..... - - - .... - - - - - - - - - +``` - ..... +If you can't or don't want to use autolink, you can also manually link the library using the instructions below (click on the arrow to show them): -``` +
+Manually link the library on Android -In `android/settings.gradle` +Make the following changes: -```gradle -... +#### `android/settings.gradle` +```groovy include ':react-native-twilio-programmable-voice' -project(':react-native-twilio-programmable-voice').projectDir = file('../node_modules/react-native-twilio-programmable-voice/android') +project(':react-native-twilio-programmable-voice').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-twilio-programmable-voice/android') +``` + +#### `android/app/build.gradle` + +```groovy +dependencies { + implementation project(':react-native-twilio-programmable-voice') +} ``` -Register module (in `MainApplication.java`) +#### `android/app/src/main/.../MainApplication.java` +On top, where imports are: ```java import com.hoxfon.react.RNTwilioVoice.TwilioVoicePackage; // <--- Import Package +``` -public class MainApplication extends Application implements ReactApplication { +Add the `TwilioVoicePackage` class to your list of exported packages. - private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { - @Override - protected boolean getUseDeveloperSupport() { - return BuildConfig.DEBUG; - } - - @Override - protected List getPackages() { - return Arrays.asList( - new MainReactPackage(), - new TwilioVoicePackage() // <---- Add the Package : by default it will ask microphone permissions - // new TwilioVoicePackage(false) // <---- pass false to handle microphone permissions in your application - ); - } - }; - .... +```java +@Override +protected List getPackages() { + return Arrays.asList( + new MainReactPackage(), + new TwilioVoicePackage() // <---- Add the package + // new TwilioVoicePackage(false) // <---- pass false if you don't want to ask for microphone permissions + ); } ``` +
## Usage @@ -246,9 +218,11 @@ async function initTelephony() { console.err(err) } } - // iOS Only -function initTelephonyWithUrl(url) { - TwilioVoice.initWithTokenUrl(url) + +function initTelephonyWithToken(token) { + TwilioVoice.initWithAccessToken(token) + + // iOS only, configure CallKit try { TwilioVoice.configureCallKit({ appName: 'TwilioVoiceExample', // Required param @@ -274,19 +248,25 @@ TwilioVoice.addEventListener('deviceNotReady', function(data) { // } }) TwilioVoice.addEventListener('connectionDidConnect', function(data) { - // Android // { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', + // call_state: 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', + // call_from: string, // "+441234567890" + // call_to: string, // "client:bob" + // } +}) +TwilioVoice.addEventListener('connectionIsReconnecting', function(data) { + // { + // call_sid: string, // Twilio call sid // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // } - // iOS +}) +TwilioVoice.addEventListener('connectionDidReconnect', function(data) { // { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', - // from: string, // "+441234567890" // issue 44 (https://github.com/hoxfon/react-native-twilio-programmable-voice/issues/44) - // to: string, // "client:bob" // issue 44 (https://github.com/hoxfon/react-native-twilio-programmable-voice/issues/44) + // call_from: string, // "+441234567890" + // call_to: string, // "client:bob" // } }) TwilioVoice.addEventListener('connectionDidDisconnect', function(data: mixed) { @@ -294,44 +274,48 @@ TwilioVoice.addEventListener('connectionDidDisconnect', function(data: mixed) { // | { // err: string // } - // | Android - // { + // | { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', + // call_state: 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // err?: string, // } - // | iOS - // { - // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', - // call_from?: string, // "+441234567890" - // call_to?: string, // "client:bob" - // from?: string, // "+441234567890" // issue 44 (https://github.com/hoxfon/react-native-twilio-programmable-voice/issues/44) - // to?: string, // "client:bob" // issue 44 (https://github.com/hoxfon/react-native-twilio-programmable-voice/issues/44) - // error?: string, // issue 44 (https://github.com/hoxfon/react-native-twilio-programmable-voice/issues/44) - // } +}) +TwilioVoice.addEventListener('callStateRinging', function(data: mixed) { + // { + // call_sid: string, // Twilio call sid + // call_state: 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', + // call_from: string, // "+441234567890" + // call_to: string, // "client:bob" + // } +}) +TwilioVoice.addEventListener('callInviteCancelled', function(data: mixed) { + // { + // call_sid: string, // Twilio call sid + // call_from: string, // "+441234567890" + // call_to: string, // "client:bob" + // } }) // iOS Only TwilioVoice.addEventListener('callRejected', function(value: 'callRejected') {}) -// Android Only TwilioVoice.addEventListener('deviceDidReceiveIncoming', function(data) { // { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // } }) + // Android Only TwilioVoice.addEventListener('proximity', function(data) { // { // isNear: boolean // } }) + // Android Only TwilioVoice.addEventListener('wiredHeadset', function(data) { // { @@ -362,32 +346,52 @@ TwilioVoice.ignore() // mutedValue must be a boolean TwilioVoice.setMuted(mutedValue) +// put a call on hold +TwilioVoice.setOnHold(holdValue) + +// send digits TwilioVoice.sendDigits(digits) -// should be called after the app is initialized -// to catch incoming call when the app was in the background +// Ensure that an active call is displayed when the app comes to foreground TwilioVoice.getActiveCall() - .then(incomingCall => { - if (incomingCall){ - _deviceDidReceiveIncoming(incomingCall) + .then(activeCall => { + if (activeCall){ + _displayActiveCall(activeCall) + } + }) + +// Ensure that call invites are displayed when the app comes to foreground +TwilioVoice.getCallInvite() + .then(callInvite => { + if (callInvite){ + _handleCallInvite(callInvite) } }) + +// Unregister device with Twilio (iOS only) +TwilioVoice.unregister() ``` -## Twilio Voice SDK reference +## Help wanted + +There is no need to ask permissions to contribute. Just open an issue or provide a PR. Everybody is welcome to contribute. + +ReactNative success is directly linked to its module ecosystem. One way to make an impact is helping contributing to this module or another of the many community lead ones. + +![help wanted](images/vjeux_tweet.png "help wanted") -[iOS changelog](https://www.twilio.com/docs/api/voice-sdk/ios/changelog) +## Twilio Voice SDK reference -[Android changelog](https://www.twilio.com/docs/api/voice-sdk/android/changelog) +[iOS changelog](https://www.twilio.com/docs/voice/voip-sdk/ios/changelog) +[Android changelog](https://www.twilio.com/docs/voice/voip-sdk/android/3x-changelog) ## Credits [voice-quickstart-android](https://github.com/twilio/voice-quickstart-android) -[react-native-push-notification](https://github.com/zo0r/react-native-push-notification) - -[voice-quickstart-objc](https://github.com/twilio/voice-quickstart-objc) +[voice-quickstart-ios](https://github.com/twilio/voice-quickstart-ios) +[react-native-push-notification](https://github.com/zo0r/react-native-push-notification) ## License diff --git a/RNTwilioVoice.podspec b/RNTwilioVoice.podspec index 9b80fac6..4fa8df77 100644 --- a/RNTwilioVoice.podspec +++ b/RNTwilioVoice.podspec @@ -9,12 +9,12 @@ Pod::Spec.new do |s| s.authors = spec['author']['name'] s.homepage = spec['homepage'] s.license = spec['license'] - s.platform = :ios, "8.1" + s.platform = :ios, "10.0" s.source_files = [ "ios/RNTwilioVoice/*.h", "ios/RNTwilioVoice/*.m"] s.source = {:path => "./RNTwilioVoice"} s.dependency 'React' - s.xcconfig = { 'FRAMEWORK_SEARCH_PATHS' => '${PODS_ROOT}/TwilioVoice' } + s.xcconfig = { 'FRAMEWORK_SEARCH_PATHS' => '${PODS_ROOT}/TwilioVoice/Build/iOS' } s.frameworks = 'TwilioVoice' -end \ No newline at end of file +end diff --git a/android/build.gradle b/android/build.gradle index 878fc179..7bab9f44 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,11 +2,12 @@ buildscript { repositories { + google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.1' - classpath 'com.google.gms:google-services:3.1.2' + classpath 'com.android.tools.build:gradle:3.5.3' + classpath 'com.google.gms:google-services:4.3.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -15,21 +16,28 @@ buildscript { allprojects { repositories { + google() jcenter() - maven { - url "https://maven.google.com" - } } } apply plugin: 'com.android.library' +def DEFAULT_COMPILE_SDK_VERSION = 28 +def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3" +def DEFAULT_TARGET_SDK_VERSION = 28 +def DEFAULT_SUPPORT_LIB_VERSION = "28.0.3" + android { - compileSdkVersion 27 - buildToolsVersion "27.0.3" + compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION + buildToolsVersion rootProject.hasProperty('buildToolsVersion') ? rootProject.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } defaultConfig { minSdkVersion 16 - targetSdkVersion 27 + targetSdkVersion rootProject.hasProperty('targetSdkVersion') ? rootProject.targetSdkVersion : DEFAULT_TARGET_SDK_VERSION versionCode 1 versionName "1.0" vectorDrawables.useSupportLibrary = true @@ -43,10 +51,12 @@ android { } dependencies { - compile fileTree(include: ['*.jar'], dir: 'libs') - compile 'com.twilio:voice-android:2.0.7' - compile 'com.android.support:appcompat-v7:27.0.2' - compile 'com.facebook.react:react-native:+' - compile 'com.google.firebase:firebase-messaging:17.+' - testCompile 'junit:junit:4.12' + def supportLibVersion = rootProject.hasProperty('supportLibVersion') ? rootProject.supportLibVersion : DEFAULT_SUPPORT_LIB_VERSION + + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation 'com.twilio:voice-android:4.5.0' + implementation "com.android.support:appcompat-v7:$supportLibVersion" + implementation 'com.facebook.react:react-native:+' + implementation 'com.google.firebase:firebase-messaging:17.6.+' + testImplementation 'junit:junit:4.12' } diff --git a/android/gradle.properties b/android/gradle.properties index aac7c9b4..af6dcbe4 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -15,3 +15,5 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 04e285f3..f6405f2b 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index e69de29b..00000000 diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java index bace1249..72577a38 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java @@ -15,12 +15,13 @@ import android.os.Build; import android.os.Bundle; import android.service.notification.StatusBarNotification; -import android.support.v4.app.NotificationCompat; +import androidx.core.app.NotificationCompat; import android.util.Log; import android.view.WindowManager; import com.facebook.react.bridge.ReactApplicationContext; import com.twilio.voice.CallInvite; +import com.twilio.voice.CancelledCallInvite; import java.util.List; @@ -89,6 +90,7 @@ public Intent getLaunchIntent(ReactApplicationContext context, Boolean shouldStartNewTask, int appImportance ) { + Log.d(TAG, "getLaunchIntent intent "); Intent launchIntent = new Intent(context, getMainActivityClass(context)); int launchFlag = Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP; @@ -109,6 +111,7 @@ public Intent getLaunchIntent(ReactApplicationContext context, if (callInvite != null) { launchIntent.putExtra(INCOMING_CALL_INVITE, callInvite); } + Log.d(TAG, "getLaunchIntent return "); return launchIntent; } @@ -120,7 +123,7 @@ public void createIncomingCallNotification(ReactApplicationContext context, if (BuildConfig.DEBUG) { Log.d(TAG, "createIncomingCallNotification intent "+launchIntent.getFlags()); } - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); @@ -166,7 +169,7 @@ public void createIncomingCallNotification(ReactApplicationContext context, .putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent pendingRejectIntent = PendingIntent.getBroadcast(context, 1, rejectIntent, - PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); notificationBuilder.addAction(0, "DISMISS", pendingRejectIntent); // Answer action @@ -175,7 +178,7 @@ public void createIncomingCallNotification(ReactApplicationContext context, .putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent pendingAnswerIntent = PendingIntent.getBroadcast(context, 0, answerIntent, - PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); notificationBuilder.addAction(R.drawable.ic_call_white_24dp, "ANSWER", pendingAnswerIntent); notificationManager.notify(notificationId, notificationBuilder.build()); @@ -187,9 +190,10 @@ public void initCallNotificationsChannel(NotificationManager notificationManager return; } NotificationChannel channel = new NotificationChannel(VOICE_CHANNEL, - "Primary Voice Channel", NotificationManager.IMPORTANCE_DEFAULT); + "4 - Primary Voice Channel", NotificationManager.IMPORTANCE_DEFAULT); channel.setLightColor(Color.GREEN); channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); + channel.setShowBadge(false); notificationManager.createNotificationChannel(channel); } @@ -205,11 +209,11 @@ public void createMissedCallNotification(ReactApplicationContext context, CallIn intent.setAction(ACTION_MISSED_CALL) .putExtra(INCOMING_CALL_NOTIFICATION_ID, MISSED_CALLS_NOTIFICATION_ID) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); Intent clearMissedCallsCountIntent = new Intent(ACTION_CLEAR_MISSED_CALLS_COUNT) .putExtra(INCOMING_CALL_NOTIFICATION_ID, CLEAR_MISSED_CALLS_NOTIFICATION_ID); - PendingIntent clearMissedCallsCountPendingIntent = PendingIntent.getBroadcast(context, 0, clearMissedCallsCountIntent, 0); + PendingIntent clearMissedCallsCountPendingIntent = PendingIntent.getBroadcast(context, 0, clearMissedCallsCountIntent, PendingIntent.FLAG_IMMUTABLE); /* * Pass the notification id and call sid to use as an identifier to open the notification */ @@ -269,14 +273,14 @@ public void createHangupLocalNotification(ReactApplicationContext context, Strin context, 0, new Intent(ACTION_HANGUP_CALL).putExtra(INCOMING_CALL_NOTIFICATION_ID, HANGUP_NOTIFICATION_ID), - PendingIntent.FLAG_UPDATE_CURRENT + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); Intent launchIntent = new Intent(context, getMainActivityClass(context)); launchIntent.setAction(ACTION_INCOMING_CALL) .putExtra(INCOMING_CALL_NOTIFICATION_ID, HANGUP_NOTIFICATION_ID) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); /* * Pass the notification id and call sid to use as an identifier to cancel the @@ -307,12 +311,18 @@ public void createHangupLocalNotification(ReactApplicationContext context, Strin } public void removeIncomingCallNotification(ReactApplicationContext context, - CallInvite callInvite, + CancelledCallInvite callInvite, int notificationId) { - Log.d(TAG, "removeIncomingCallNotification"); + if (BuildConfig.DEBUG) { + Log.d(TAG, "removeIncomingCallNotification"); + } + if (context == null) { + Log.e(TAG, "Context is null"); + return; + } NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - if (callInvite != null && callInvite.getState() == CallInvite.State.PENDING) { + if (callInvite != null) { /* * If the incoming call message was cancelled then remove the notification by matching * it with the call sid from the list of notifications in the notification drawer. diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java index 800ef6db..753c2b32 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java @@ -1,6 +1,6 @@ package com.hoxfon.react.RNTwilioVoice; -import android.support.annotation.Nullable; +import androidx.annotation.Nullable; import android.util.Log; import com.facebook.react.bridge.ReactApplicationContext; @@ -21,6 +21,11 @@ public class EventManager { public static final String EVENT_CONNECTION_DID_CONNECT = "connectionDidConnect"; public static final String EVENT_CONNECTION_DID_DISCONNECT = "connectionDidDisconnect"; public static final String EVENT_DEVICE_DID_RECEIVE_INCOMING = "deviceDidReceiveIncoming"; + public static final String EVENT_CALL_STATE_RINGING = "callStateRinging"; + public static final String EVENT_CALL_INVITE_CANCELLED = "callInviteCancelled"; + public static final String EVENT_CONNECTION_IS_RECONNECTING = "connectionIsReconnecting"; + public static final String EVENT_CONNECTION_DID_RECONNECT = "connectionDidReconnect"; + public EventManager(ReactApplicationContext context) { mContext = context; diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/SoundPoolManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/SoundPoolManager.java index cda96606..8520a746 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/SoundPoolManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/SoundPoolManager.java @@ -4,6 +4,7 @@ import android.media.Ringtone; import android.media.RingtoneManager; import android.net.Uri; +import android.util.Log; public class SoundPoolManager { diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java index 774ad936..3765bb78 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java @@ -15,9 +15,10 @@ import android.media.AudioManager; import android.os.Build; -import android.support.v4.app.ActivityCompat; -import android.support.v4.content.ContextCompat; -import android.support.v4.content.LocalBroadcastManager; +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import android.util.Log; import android.view.Window; import android.view.WindowManager; @@ -38,11 +39,16 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; -import com.google.firebase.FirebaseApp; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; +import com.twilio.voice.AcceptOptions; import com.twilio.voice.Call; import com.twilio.voice.CallException; import com.twilio.voice.CallInvite; +import com.twilio.voice.CancelledCallInvite; +import com.twilio.voice.ConnectOptions; import com.twilio.voice.LogLevel; import com.twilio.voice.RegistrationException; import com.twilio.voice.RegistrationListener; @@ -56,6 +62,10 @@ import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_DEVICE_DID_RECEIVE_INCOMING; import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_DEVICE_NOT_READY; import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_DEVICE_READY; +import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CALL_STATE_RINGING; +import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CALL_INVITE_CANCELLED; +import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CONNECTION_IS_RECONNECTING; +import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CONNECTION_DID_RECONNECT; public class TwilioVoiceModule extends ReactContextBaseJavaModule implements ActivityEventListener, LifecycleEventListener { @@ -75,6 +85,8 @@ public class TwilioVoiceModule extends ReactContextBaseJavaModule implements Act public static final String INCOMING_CALL_INVITE = "INCOMING_CALL_INVITE"; public static final String INCOMING_CALL_NOTIFICATION_ID = "INCOMING_CALL_NOTIFICATION_ID"; public static final String NOTIFICATION_TYPE = "NOTIFICATION_TYPE"; + public static final String CANCELLED_CALL_INVITE = "CANCELLED_CALL_INVITE"; + public static final String ACTION_INCOMING_CALL = "com.hoxfon.react.TwilioVoice.INCOMING_CALL"; public static final String ACTION_FCM_TOKEN = "com.hoxfon.react.TwilioVoice.ACTION_FCM_TOKEN"; @@ -82,6 +94,7 @@ public class TwilioVoiceModule extends ReactContextBaseJavaModule implements Act public static final String ACTION_ANSWER_CALL = "com.hoxfon.react.TwilioVoice.ANSWER_CALL"; public static final String ACTION_REJECT_CALL = "com.hoxfon.react.TwilioVoice.REJECT_CALL"; public static final String ACTION_HANGUP_CALL = "com.hoxfon.react.TwilioVoice.HANGUP_CALL"; + public static final String ACTION_CANCEL_CALL_INVITE = "com.hoxfon.react.TwilioVoice.CANCEL_CALL_INVITE"; public static final String ACTION_CLEAR_MISSED_CALLS_COUNT = "com.hoxfon.react.TwilioVoice.CLEAR_MISSED_CALLS_COUNT"; public static final String CALL_SID_KEY = "CALL_SID"; @@ -214,6 +227,34 @@ public void onError(RegistrationException error, String accessToken, String fcmT private Call.Listener callListener() { return new Call.Listener() { + /* + * This callback is emitted once before the Call.Listener.onConnected() callback when + * the callee is being alerted of a Call. The behavior of this callback is determined by + * the answerOnBridge flag provided in the Dial verb of your TwiML application + * associated with this client. If the answerOnBridge flag is false, which is the + * default, the Call.Listener.onConnected() callback will be emitted immediately after + * Call.Listener.onRinging(). If the answerOnBridge flag is true, this will cause the + * call to emit the onConnected callback only after the call is answered. + * See answeronbridge for more details on how to use it with the Dial TwiML verb. If the + * twiML response contains a Say verb, then the call will emit the + * Call.Listener.onConnected callback immediately after Call.Listener.onRinging() is + * raised, irrespective of the value of answerOnBridge being set to true or false + */ + @Override + public void onRinging(Call call) { + // TODO test this with JS app + if (BuildConfig.DEBUG) { + Log.d(TAG, "CALL RINGING callListener().onRinging call state = "+call.getState()); + Log.d(TAG, call.toString()); + } + WritableMap params = Arguments.createMap(); + if (call != null) { + params.putString("call_sid", call.getSid()); + params.putString("call_from", call.getFrom()); + } + eventManager.sendEvent(EVENT_CALL_STATE_RINGING, params); + } + @Override public void onConnected(Call call) { if (BuildConfig.DEBUG) { @@ -242,17 +283,53 @@ public void onConnected(Call call) { eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params); } + /** + * `onReconnecting()` callback is raised when a network change is detected and Call is already in `CONNECTED` + * `Call.State`. If the call is in `CONNECTING` or `RINGING` when network change happened the SDK will continue + * attempting to connect, but a reconnect event will not be raised. + */ + @Override + public void onReconnecting(@NonNull Call call, @NonNull CallException callException) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "CALL RECONNECTING callListener().onReconnecting call state = "+call.getState()); + } + WritableMap params = Arguments.createMap(); + if (call != null) { + params.putString("call_sid", call.getSid()); + params.putString("call_from", call.getFrom()); + params.putString("call_to", call.getTo()); + } + eventManager.sendEvent(EVENT_CONNECTION_IS_RECONNECTING, params); + + } + + /** + * The call is successfully reconnected after reconnecting attempt. + */ + @Override + public void onReconnected(@NonNull Call call) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "CALL RECONNECTED callListener().onReconnected call state = "+call.getState()); + } + WritableMap params = Arguments.createMap(); + if (call != null) { + params.putString("call_sid", call.getSid()); + params.putString("call_from", call.getFrom()); + params.putString("call_to", call.getTo()); + } + eventManager.sendEvent(EVENT_CONNECTION_DID_RECONNECT, params); + } + @Override public void onDisconnected(Call call, CallException error) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "CALL DISCONNECTED callListener().onDisconnected call state = "+call.getState()); + } unsetAudioFocus(); proximityManager.stopProximitySensor(); headsetManager.stopWiredHeadsetEvent(getReactApplicationContext()); callAccepted = false; - if (BuildConfig.DEBUG) { - Log.d(TAG, "call disconnected"); - } - WritableMap params = Arguments.createMap(); String callSid = ""; if (call != null) { @@ -278,14 +355,15 @@ public void onDisconnected(Call call, CallException error) { @Override public void onConnectFailure(Call call, CallException error) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "CALL FAILURE callListener().onConnectFailure call state = "+call.getState()); + } unsetAudioFocus(); proximityManager.stopProximitySensor(); callAccepted = false; - if (BuildConfig.DEBUG) { - Log.d(TAG, "connect failure"); - } - Log.e(TAG, String.format("CallListener onDisconnected error: %d, %s", + + Log.e(TAG, String.format("CallListener onConnectFailure error: %d, %s", error.getErrorCode(), error.getMessage())); WritableMap params = Arguments.createMap(); @@ -316,6 +394,7 @@ private void registerReceiver() { if (!isReceiverRegistered) { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ACTION_INCOMING_CALL); + intentFilter.addAction(ACTION_CANCEL_CALL_INVITE); intentFilter.addAction(ACTION_MISSED_CALL); LocalBroadcastManager.getInstance(getReactApplicationContext()).registerReceiver( voiceBroadcastReceiver, intentFilter); @@ -378,94 +457,71 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } private void handleIncomingCallIntent(Intent intent) { - if (intent == null || intent.getAction() == null) { - Log.e(TAG, "handleIncomingCallIntent intent is null"); - return; - } - - if (intent.getAction().equals(ACTION_INCOMING_CALL)) { - activeCallInvite = intent.getParcelableExtra(INCOMING_CALL_INVITE); - - if (activeCallInvite != null && (activeCallInvite.getState() == CallInvite.State.PENDING)) { - callAccepted = false; + try { + if (intent.getAction().equals(ACTION_INCOMING_CALL)) { if (BuildConfig.DEBUG) { - Log.d(TAG, "handleIncomingCallIntent state = PENDING"); - } - SoundPoolManager.getInstance(getReactApplicationContext()).playRinging(); - - if (getReactApplicationContext().getCurrentActivity() != null) { - Window window = getReactApplicationContext().getCurrentActivity().getWindow(); - window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - ); + Log.d(TAG, "handleIncomingCallIntent"); } - // send a JS event ONLY if the app's importance is FOREGROUND or SERVICE - // at startup the app would try to fetch the activeIncoming calls - int appImportance = callNotificationManager.getApplicationImportance(getReactApplicationContext()); - if (appImportance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND || - appImportance == RunningAppProcessInfo.IMPORTANCE_SERVICE) { - - WritableMap params = Arguments.createMap(); - params.putString("call_sid", activeCallInvite.getCallSid()); - params.putString("call_from", activeCallInvite.getFrom()); - params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); - eventManager.sendEvent(EVENT_DEVICE_DID_RECEIVE_INCOMING, params); + activeCallInvite = intent.getParcelableExtra(INCOMING_CALL_INVITE); + if (activeCallInvite != null) { + callAccepted = false; + + // SoundPoolManager.getInstance(getReactApplicationContext()).playRinging(); + + if (getReactApplicationContext().getCurrentActivity() != null) { + Window window = getReactApplicationContext().getCurrentActivity().getWindow(); + window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED + ); + } + // send a JS event ONLY if the app's importance is FOREGROUND or SERVICE + // at startup the app would try to fetch the activeIncoming calls + int appImportance = callNotificationManager.getApplicationImportance(getReactApplicationContext()); + if (appImportance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND || + appImportance == RunningAppProcessInfo.IMPORTANCE_SERVICE) { + + WritableMap params = Arguments.createMap(); + params.putString("call_sid", activeCallInvite.getCallSid()); + params.putString("call_from", activeCallInvite.getFrom()); + params.putString("call_to", activeCallInvite.getTo()); // TODO check if needed + eventManager.sendEvent(EVENT_DEVICE_DID_RECEIVE_INCOMING, params); + } + } else { + // TODO evaluate what more is needed at this point? + Log.e(TAG, "ACTION_INCOMING_CALL but not active call"); } - - - } else { + } else if (intent.getAction().equals(ACTION_CANCEL_CALL_INVITE)) { + SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); if (BuildConfig.DEBUG) { - Log.d(TAG, "====> BEGIN handleIncomingCallIntent when activeCallInvite != PENDING"); + Log.d(TAG, "activeCallInvite was cancelled by " + activeCallInvite.getFrom()); } - // this block is executed when the callInvite is cancelled and: - // - the call is answered (activeCall != null) - // - the call is rejected - - SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); - - // the call is not active yet - if (activeCall == null) { - - if (activeCallInvite != null) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "activeCallInvite state = " + activeCallInvite.getState()); - } - if (BuildConfig.DEBUG) { - Log.d(TAG, "activeCallInvite was cancelled by " + activeCallInvite.getFrom()); - } - if (!callAccepted) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "creating a missed call, activeCallInvite state: " + activeCallInvite.getState()); - } - callNotificationManager.createMissedCallNotification(getReactApplicationContext(), activeCallInvite); - int appImportance = callNotificationManager.getApplicationImportance(getReactApplicationContext()); - if (appImportance != RunningAppProcessInfo.IMPORTANCE_BACKGROUND) { - WritableMap params = Arguments.createMap(); - params.putString("call_sid", activeCallInvite.getCallSid()); - params.putString("call_from", activeCallInvite.getFrom()); - params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); - eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); - } - } - } - clearIncomingNotification(activeCallInvite); - } else { + if (!callAccepted) { if (BuildConfig.DEBUG) { - Log.d(TAG, "activeCallInvite was answered. Call " + activeCall); + Log.d(TAG, "creating a missed call"); + } + callNotificationManager.createMissedCallNotification(getReactApplicationContext(), activeCallInvite); + int appImportance = callNotificationManager.getApplicationImportance(getReactApplicationContext()); + if (appImportance != RunningAppProcessInfo.IMPORTANCE_BACKGROUND) { + WritableMap params = Arguments.createMap(); + params.putString("call_sid", activeCallInvite.getCallSid()); + params.putString("call_from", activeCallInvite.getFrom()); + params.putString("call_to", activeCallInvite.getTo()); + params.putString("call_state", Call.State.DISCONNECTED.toString()); + eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); } } + clearIncomingNotification(activeCallInvite.getCallSid()); + } else if (intent.getAction().equals(ACTION_FCM_TOKEN)) { if (BuildConfig.DEBUG) { - Log.d(TAG, "====> END"); + Log.d(TAG, "handleIncomingCallIntent ACTION_FCM_TOKEN"); } + registerForCallInvites(); } - } else if (intent.getAction().equals(ACTION_FCM_TOKEN)) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "handleIncomingCallIntent ACTION_FCM_TOKEN"); - } - registerForCallInvites(); + } catch (Exception e) { + //TODO: handle exception + Log.d(TAG, "handleIncomingCallIntent error"); } + } private class VoiceBroadcastReceiver extends BroadcastReceiver { @@ -473,11 +529,21 @@ private class VoiceBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "VoiceBroadcastReceiver.onReceive "+action); + } if (action.equals(ACTION_INCOMING_CALL)) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "VoiceBroadcastReceiver.onReceive ACTION_INCOMING_CALL. Intent "+ intent.getExtras()); - } handleIncomingCallIntent(intent); + } else if (action.equals(ACTION_CANCEL_CALL_INVITE)) { + CancelledCallInvite cancelledCallInvite = intent.getParcelableExtra(CANCELLED_CALL_INVITE); + clearIncomingNotification(cancelledCallInvite.getCallSid()); + WritableMap params = Arguments.createMap(); + if (cancelledCallInvite != null) { + params.putString("call_sid", cancelledCallInvite.getCallSid()); + params.putString("call_from", cancelledCallInvite.getFrom()); + params.putString("call_to", cancelledCallInvite.getTo()); + } + eventManager.sendEvent(EVENT_CALL_INVITE_CANCELLED, params); } else if (action.equals(ACTION_MISSED_CALL)) { SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences(PREFERENCE_KEY, Context.MODE_PRIVATE); SharedPreferences.Editor sharedPrefEditor = sharedPref.edit(); @@ -494,15 +560,16 @@ public void initWithAccessToken(final String accessToken, Promise promise) { if (accessToken.equals("")) { promise.reject(new JSApplicationIllegalArgumentException("Invalid access token")); return; - } - + } + if(!checkPermissionForMicrophone()) { - promise.reject(new AssertionException("Can't init without microphone permission")); - } + promise.reject(new AssertionException("Allow microphone permission")); + return; + } TwilioVoiceModule.this.accessToken = accessToken; if (BuildConfig.DEBUG) { - Log.d(TAG, "initWithAccessToken ACTION_FCM_TOKEN"); + Log.d(TAG, "initWithAccessToken"); } registerForCallInvites(); WritableMap params = Arguments.createMap(); @@ -510,21 +577,19 @@ public void initWithAccessToken(final String accessToken, Promise promise) { promise.resolve(params); } - private void clearIncomingNotification(CallInvite callInvite) { + private void clearIncomingNotification(String callSid) { if (BuildConfig.DEBUG) { - Log.d(TAG, "clearIncomingNotification() callInvite state: "+ callInvite.getState()); + Log.d(TAG, "clearIncomingNotification() callSid: "+ callSid); } - if (callInvite != null && callInvite.getCallSid() != null) { - // remove incoming call notification - String notificationKey = INCOMING_NOTIFICATION_PREFIX + callInvite.getCallSid(); - int notificationId = 0; - if (TwilioVoiceModule.callNotificationMap.containsKey(notificationKey)) { - notificationId = TwilioVoiceModule.callNotificationMap.get(notificationKey); - } - callNotificationManager.removeIncomingCallNotification(getReactApplicationContext(), null, notificationId); - TwilioVoiceModule.callNotificationMap.remove(notificationKey); + // remove incoming call notification + String notificationKey = INCOMING_NOTIFICATION_PREFIX + callSid; + int notificationId = 0; + if (TwilioVoiceModule.callNotificationMap.containsKey(notificationKey)) { + notificationId = TwilioVoiceModule.callNotificationMap.get(notificationKey); } -// activeCallInvite = null; + callNotificationManager.removeIncomingCallNotification(getReactApplicationContext(), null, notificationId); + TwilioVoiceModule.callNotificationMap.remove(notificationKey); + activeCallInvite = null; } /* @@ -533,47 +598,52 @@ private void clearIncomingNotification(CallInvite callInvite) { * If a valid google-services.json has not been provided or the FirebaseInstanceId has not been * initialized the fcmToken will be null. * - * In the case where the FirebaseInstanceId has not yet been initialized the - * VoiceFirebaseInstanceIDService.onTokenRefresh should result in a LocalBroadcast to this - * activity which will attempt registerForCallInvites again. - * */ private void registerForCallInvites() { - FirebaseApp.initializeApp(getReactApplicationContext()); - final String fcmToken = FirebaseInstanceId.getInstance().getToken(); - if (fcmToken != null) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "Registering with FCM"); - } - Voice.register(getReactApplicationContext(), accessToken, Voice.RegistrationChannel.FCM, fcmToken, registrationListener); - } + FirebaseInstanceId.getInstance().getInstanceId() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (!task.isSuccessful()) { + Log.w(TAG, "getInstanceId failed", task.getException()); + return; + } + + // Get new Instance ID token + String fcmToken = task.getResult().getToken(); + if (fcmToken != null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Registering with FCM"); + } + Voice.register(accessToken, Voice.RegistrationChannel.FCM, fcmToken, registrationListener); + } + } + }); } @ReactMethod public void accept() { callAccepted = true; SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); - if (activeCallInvite != null){ - if (activeCallInvite.getState() == CallInvite.State.PENDING) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "accept() activeCallInvite.getState() PENDING"); - } - activeCallInvite.accept(getReactApplicationContext(), callListener); - clearIncomingNotification(activeCallInvite); - } else { - // when the user answers a call from a notification before the react-native App - // is completely initialised, and the first event has been skipped - // re-send connectionDidConnect message to JS - WritableMap params = Arguments.createMap(); - params.putString("call_sid", activeCallInvite.getCallSid()); - params.putString("call_from", activeCallInvite.getFrom()); - params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); - callNotificationManager.createHangupLocalNotification(getReactApplicationContext(), - activeCallInvite.getCallSid(), - activeCallInvite.getFrom()); - eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params); - } + if (activeCallInvite != null) { + AcceptOptions acceptOptions = new AcceptOptions.Builder() + .enableDscp(true) + .build(); + activeCallInvite.accept(getReactApplicationContext(), acceptOptions, callListener); + clearIncomingNotification(activeCallInvite.getCallSid()); + + // TODO check whether this block is needed +// // when the user answers a call from a notification before the react-native App +// // is completely initialised, and the first event has been skipped +// // re-send connectionDidConnect message to JS +// WritableMap params = Arguments.createMap(); +// params.putString("call_sid", activeCallInvite.getCallSid()); +// params.putString("call_from", activeCallInvite.getFrom()); +// params.putString("call_to", activeCallInvite.getTo()); +// callNotificationManager.createHangupLocalNotification(getReactApplicationContext(), +// activeCallInvite.getCallSid(), +// activeCallInvite.getFrom()); +// eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params); } else { eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, null); } @@ -584,13 +654,15 @@ public void reject() { callAccepted = false; SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); WritableMap params = Arguments.createMap(); - if (activeCallInvite != null){ + if (activeCallInvite != null) { params.putString("call_sid", activeCallInvite.getCallSid()); params.putString("call_from", activeCallInvite.getFrom()); params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); + params.putString("call_state", "DISCONNECTED"); + // TODO check if DISCONNECTED should be REJECTED + // params.putString("call_state", "REJECTED"); activeCallInvite.reject(getReactApplicationContext()); - clearIncomingNotification(activeCallInvite); + clearIncomingNotification(activeCallInvite.getCallSid()); } eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); } @@ -600,12 +672,12 @@ public void ignore() { callAccepted = false; SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); WritableMap params = Arguments.createMap(); - if (activeCallInvite != null){ + if (activeCallInvite != null) { params.putString("call_sid", activeCallInvite.getCallSid()); params.putString("call_from", activeCallInvite.getFrom()); params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); - clearIncomingNotification(activeCallInvite); + params.putString("call_state", "BUSY"); + clearIncomingNotification(activeCallInvite.getCallSid()); } eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); } @@ -661,7 +733,12 @@ public void connect(ReadableMap params) { } } - activeCall = Voice.call(getReactApplicationContext(), accessToken, twiMLParams, callListener); + ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken) + .enableDscp(true) + .params(twiMLParams) + .build(); + + activeCall = Voice.connect(getReactApplicationContext(), connectOptions, callListener); } @ReactMethod @@ -673,9 +750,9 @@ public void disconnect() { } @ReactMethod - public void setMuted(Boolean muteValue) { + public void setMuted(Boolean value) { if (activeCall != null) { - activeCall.mute(muteValue); + activeCall.mute(value); } } @@ -693,22 +770,30 @@ public void getActiveCall(Promise promise) { Log.d(TAG, "Active call found state = "+activeCall.getState()); } WritableMap params = Arguments.createMap(); + String toNum = activeCall.getTo(); + if (toNum == null) { + toNum = toNumber; + } params.putString("call_sid", activeCall.getSid()); params.putString("call_from", activeCall.getFrom()); - params.putString("call_to", activeCall.getTo()); + params.putString("call_to", toNum); params.putString("call_state", activeCall.getState().name()); promise.resolve(params); return; } + promise.resolve(null); + } + + @ReactMethod + public void getCallInvite(Promise promise) { if (activeCallInvite != null) { if (BuildConfig.DEBUG) { - Log.d(TAG, "Active call invite found state = "+activeCallInvite.getState()); + Log.d(TAG, "Call invite found "+ activeCallInvite); } WritableMap params = Arguments.createMap(); params.putString("call_sid", activeCallInvite.getCallSid()); params.putString("call_from", activeCallInvite.getFrom()); params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); promise.resolve(params); return; } @@ -722,8 +807,17 @@ public void setSpeakerPhone(Boolean value) { audioManager.setSpeakerphoneOn(value); } + @ReactMethod + public void setOnHold(Boolean value) { + if (activeCall != null) { + activeCall.hold(value); + } + } + private void setAudioFocus() { if (audioManager == null) { + audioManager.setMode(originalAudioMode); + audioManager.abandonAudioFocus(null); return; } originalAudioMode = audioManager.getMode(); @@ -743,11 +837,12 @@ public void onAudioFocusChange(int i) { } .build(); audioManager.requestAudioFocus(focusRequest); } else { - audioManager.requestAudioFocus( - null, - AudioManager.STREAM_VOICE_CALL, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE - ); + int focusRequestResult = audioManager.requestAudioFocus(new AudioManager.OnAudioFocusChangeListener() { + @Override + public void onAudioFocusChange(int focusChange) {} + }, + AudioManager.STREAM_VOICE_CALL, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); } /* * Start by setting MODE_IN_COMMUNICATION as default audio mode. It is @@ -760,6 +855,8 @@ public void onAudioFocusChange(int i) { } private void unsetAudioFocus() { if (audioManager == null) { + audioManager.setMode(originalAudioMode); + audioManager.abandonAudioFocus(null); return; } audioManager.setMode(originalAudioMode); @@ -778,14 +875,15 @@ private boolean checkPermissionForMicrophone() { } private void requestPermissionForMicrophone() { - if (getCurrentActivity() != null) { - if (ActivityCompat.shouldShowRequestPermissionRationale(getCurrentActivity(), Manifest.permission.RECORD_AUDIO)) { - // Snackbar.make(coordinatorLayout, - // "Microphone permissions needed. Please allow in your application settings.", - // SNACKBAR_DURATION).show(); - } else { - ActivityCompat.requestPermissions(getCurrentActivity(), new String[]{Manifest.permission.RECORD_AUDIO}, MIC_PERMISSION_REQUEST_CODE); - } + if (getCurrentActivity() == null) { + return; + } + if (ActivityCompat.shouldShowRequestPermissionRationale(getCurrentActivity(), Manifest.permission.RECORD_AUDIO)) { +// Snackbar.make(coordinatorLayout, +// "Microphone permissions needed. Please allow in your application settings.", +// SNACKBAR_DURATION).show(); + } else { + ActivityCompat.requestPermissions(getCurrentActivity(), new String[]{Manifest.permission.RECORD_AUDIO}, MIC_PERMISSION_REQUEST_CODE); } } } diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseInstanceIDService.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseInstanceIDService.java deleted file mode 100644 index 9154661c..00000000 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseInstanceIDService.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.hoxfon.react.RNTwilioVoice.fcm; - -import android.content.Intent; -import android.support.v4.content.LocalBroadcastManager; -import android.util.Log; - -import com.google.firebase.iid.FirebaseInstanceId; -import com.google.firebase.iid.FirebaseInstanceIdService; -import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_FCM_TOKEN; -import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG; - -public class VoiceFirebaseInstanceIDService extends FirebaseInstanceIdService { - - /** - * Called if InstanceID token is updated. This may occur if the security of - * the previous token had been compromised. Note that this is called when the InstanceID token - * is initially generated so this is where you would retrieve the token. - */ - // [START refresh_token] - @Override - public void onTokenRefresh() { - // Get updated InstanceID token. - String refreshedToken = FirebaseInstanceId.getInstance().getToken(); - Log.d(TAG, "Refreshed token: " + refreshedToken); - - // Notify Activity of FCM token - Intent intent = new Intent(ACTION_FCM_TOKEN); - LocalBroadcastManager.getInstance(this).sendBroadcast(intent); - } - // [END refresh_token] - - /** - * Persist token to third-party servers. - * - * Modify this method to associate the user's FCM InstanceID token with any server-side account - * maintained by your application. - * - * @param token The new token. - */ - private void sendRegistrationToServer(String token) { - // TODO: Implement this method to send token to your app server. - } -} diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java index b50facdc..9413f432 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java @@ -1,13 +1,11 @@ package com.hoxfon.react.RNTwilioVoice.fcm; -import android.annotation.TargetApi; - import android.app.ActivityManager; import android.content.Intent; -import android.os.Build; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.support.v4.content.LocalBroadcastManager; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; import android.util.Log; import com.facebook.react.ReactApplication; @@ -20,7 +18,7 @@ import com.hoxfon.react.RNTwilioVoice.BuildConfig; import com.hoxfon.react.RNTwilioVoice.CallNotificationManager; import com.twilio.voice.CallInvite; -import com.twilio.voice.MessageException; +import com.twilio.voice.CancelledCallInvite; import com.twilio.voice.MessageListener; import com.twilio.voice.Voice; @@ -28,144 +26,156 @@ import java.util.Random; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_FCM_TOKEN; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_INCOMING_CALL; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_CANCEL_CALL_INVITE; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_INVITE; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.CANCELLED_CALL_INVITE; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_NOTIFICATION_ID; import com.hoxfon.react.RNTwilioVoice.SoundPoolManager; public class VoiceFirebaseMessagingService extends FirebaseMessagingService { - private CallNotificationManager callNotificationManager; - - @Override - public void onCreate() { - super.onCreate(); - callNotificationManager = new CallNotificationManager(); + private CallNotificationManager callNotificationManager; + + @Override + public void onCreate() { + super.onCreate(); + callNotificationManager = new CallNotificationManager(); + } + + @Override + public void onNewToken(String token) { + Log.d(TAG, "Refreshed token: " + token); + + // Notify Activity of FCM token + Intent intent = new Intent(ACTION_FCM_TOKEN); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + + /** + * Called when message is received. + * + * @param remoteMessage Object representing the message received from Firebase Cloud Messaging. + */ + @Override + public void onMessageReceived(RemoteMessage remoteMessage) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Bundle data: " + remoteMessage.getData()); } - /** - * Called when message is received. - * - * @param remoteMessage Object representing the message received from Firebase Cloud Messaging. - */ - @Override - public void onMessageReceived(RemoteMessage remoteMessage) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "Bundle data: " + remoteMessage.getData()); - } - - // Check if message contains a data payload. - if (remoteMessage.getData().size() > 0) { - Map data = remoteMessage.getData(); - - // 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(); - - Voice.handleMessage(this, 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. - // Namely, DevServerHelper constructs a Handler() without a Looper, which triggers: - // "Can't create handler inside thread that has not called Looper.prepare()" - Handler handler = new Handler(Looper.getMainLooper()); - handler.post(new Runnable() { - public void run() { - // Construct and load our normal React JS code bundle - ReactInstanceManager mReactInstanceManager = ((ReactApplication) getApplication()).getReactNativeHost().getReactInstanceManager(); - ReactContext context = mReactInstanceManager.getCurrentReactContext(); - // If it's constructed, send a notification - if (context != null) { - int appImportance = callNotificationManager.getApplicationImportance((ReactApplicationContext)context); - if (BuildConfig.DEBUG) { - Log.d(TAG, "CONTEXT present appImportance = " + appImportance); - } - Intent launchIntent = callNotificationManager.getLaunchIntent( - (ReactApplicationContext)context, - notificationId, - callInvite, - false, - appImportance - ); - // app is not in foreground - if (appImportance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { - context.startActivity(launchIntent); - } - VoiceFirebaseMessagingService.this.handleIncomingCall((ReactApplicationContext)context, notificationId, callInvite, launchIntent); - } else { - // Otherwise wait for construction, then handle the incoming call - mReactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() { - public void onReactContextInitialized(ReactContext context) { - int appImportance = callNotificationManager.getApplicationImportance((ReactApplicationContext)context); - if (BuildConfig.DEBUG) { - Log.d(TAG, "CONTEXT not present appImportance = " + appImportance); - } - Intent launchIntent = callNotificationManager.getLaunchIntent((ReactApplicationContext)context, notificationId, callInvite, true, appImportance); - context.startActivity(launchIntent); - VoiceFirebaseMessagingService.this.handleIncomingCall((ReactApplicationContext)context, notificationId, callInvite, launchIntent); - } - }); - if (!mReactInstanceManager.hasStartedCreatingInitialContext()) { - // Construct it in the background - mReactInstanceManager.createReactContextInBackground(); - } - } - } - }); - } - - @Override - public void onError(MessageException messageException) { - Log.e(TAG, "Error handling FCM message" + messageException.toString()); + // Check if message contains a data payload. + if (remoteMessage.getData().size() > 0) { + Map data = remoteMessage.getData(); + + // 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(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. + // Namely, DevServerHelper constructs a Handler() without a Looper, which triggers: + // "Can't create handler inside thread that has not called Looper.prepare()" + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + public void run() { + // Construct and load our normal React JS code bundle + ReactInstanceManager mReactInstanceManager = ((ReactApplication) getApplication()).getReactNativeHost().getReactInstanceManager(); + ReactContext context = mReactInstanceManager.getCurrentReactContext(); + // If it's constructed, send a notification + if (context != null) { + + Intent callKeepIntent = new Intent(); + callKeepIntent.setAction("nl.xguard.alarm.BACKGROUND_CALL"); + callKeepIntent.setPackage("nl.xguard.alarm"); + Bundle bundle = new Bundle(); + bundle.putString("callState", "PENDING"); // or "CANCELLED" + bundle.putString("callSid", callInvite.getCallSid()); + bundle.putString("handle", callInvite.getFrom()); + callKeepIntent.putExtras(bundle); + sendBroadcast(callKeepIntent); + + // Intent launchIntent = callNotificationManager.getLaunchIntent( + // (ReactApplicationContext)context, + // notificationId, + // callInvite, + // false, + // appImportance + // ); + // // app is not in foreground + // if (appImportance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { + // context.startActivity(launchIntent); + // } + + Intent intent = new Intent(ACTION_INCOMING_CALL); + intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); + intent.putExtra(INCOMING_CALL_INVITE, callInvite); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + + } else { + // Otherwise wait for construction, then handle the incoming call + mReactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() { + public void onReactContextInitialized(ReactContext context) { + + Intent callKeepIntent = new Intent(); + callKeepIntent.setAction("nl.xguard.alarm.BACKGROUND_CALL"); + callKeepIntent.setPackage("nl.xguard.alarm"); + Bundle bundle = new Bundle(); + bundle.putString("callState", "PENDING"); // or "CANCELLED" + bundle.putString("callSid", callInvite.getCallSid()); + bundle.putString("handle", callInvite.getFrom()); + callKeepIntent.putExtras(bundle); + sendBroadcast(callKeepIntent); + + Intent intent = new Intent(ACTION_INCOMING_CALL); + intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); + intent.putExtra(INCOMING_CALL_INVITE, callInvite); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + + } + }); + if (!mReactInstanceManager.hasStartedCreatingInitialContext()) { + // Construct it in the background + mReactInstanceManager.createReactContextInBackground(); } - }); + } + } + }); } - // Check if message contains a notification payload. - if (remoteMessage.getNotification() != null) { - Log.e(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody()); + @Override + public void onCancelledCallInvite(final CancelledCallInvite cancelledCallInvite) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + public void run() { + VoiceFirebaseMessagingService.this.sendCancelledCallInviteToActivity(cancelledCallInvite); + } + }); } - } + }); - private void handleIncomingCall(ReactApplicationContext context, - int notificationId, - CallInvite callInvite, - Intent launchIntent - ) { - sendIncomingCallMessageToActivity(context, callInvite, notificationId); - showNotification(context, callInvite, notificationId, launchIntent); + if (!valid) { + Log.e(TAG, "The message was not a valid Twilio Voice SDK payload: " + remoteMessage.getData()); + } } - /* - * Send the IncomingCallMessage to the TwilioVoiceModule - */ - private void sendIncomingCallMessageToActivity( - ReactApplicationContext context, - CallInvite callInvite, - int notificationId - ) { - Intent intent = new Intent(ACTION_INCOMING_CALL); - intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); - intent.putExtra(INCOMING_CALL_INVITE, callInvite); - LocalBroadcastManager.getInstance(context).sendBroadcast(intent); - } - - /* - * Show the notification in the Android notification drawer - */ - @TargetApi(20) - private void showNotification(ReactApplicationContext context, - CallInvite callInvite, - int notificationId, - Intent launchIntent - ) { - if (callInvite != null && callInvite.getState() == CallInvite.State.PENDING) { - callNotificationManager.createIncomingCallNotification(context, callInvite, notificationId, launchIntent); - } else { - SoundPoolManager.getInstance(context.getBaseContext()).stopRinging(); - callNotificationManager.removeIncomingCallNotification(context, callInvite, 0); - } + // Check if message contains a notification payload. + if (remoteMessage.getNotification() != null) { + Log.e(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody()); } -} + } + + /* + * Send the CancelledCallInvite to the TwilioVoiceModule + */ + private void sendCancelledCallInviteToActivity(CancelledCallInvite cancelledCallInvite) { + Log.d(TAG, "sendCancelledCallInviteToActivity"); + SoundPoolManager.getInstance((this)).stopRinging(); + Intent intent = new Intent(ACTION_CANCEL_CALL_INVITE); + intent.putExtra(CANCELLED_CALL_INVITE, cancelledCallInvite); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } +} \ No newline at end of file diff --git a/index.js b/index.js index 79dd8841..8cb7bad9 100644 --- a/index.js +++ b/index.js @@ -16,8 +16,11 @@ const _eventHandlers = { deviceNotReady: new Map(), deviceDidReceiveIncoming: new Map(), connectionDidConnect: new Map(), + connectionIsReconnecting: new Map(), + connectionDidReconnect: new Map(), connectionDidDisconnect: new Map(), - //iOS specific + callStateRinging: new Map(), + callInviteCancelled: new Map(), callRejected: new Map(), } @@ -44,17 +47,10 @@ const Twilio = { } return result }, - initWithTokenUrl(url) { - if (Platform.OS === IOS) { - TwilioVoice.initWithAccessTokenUrl(url) - } - }, connect(params = {}) { TwilioVoice.connect(params) }, - disconnect() { - TwilioVoice.disconnect() - }, + disconnect: TwilioVoice.disconnect, accept() { if (Platform.OS === IOS) { return @@ -73,23 +69,17 @@ const Twilio = { } TwilioVoice.ignore() }, - setMuted(isMuted) { - TwilioVoice.setMuted(isMuted) - }, - setSpeakerPhone(value) { - TwilioVoice.setSpeakerPhone(value) - }, - sendDigits(digits) { - TwilioVoice.sendDigits(digits) - }, + setMuted: TwilioVoice.setMuted, + setSpeakerPhone: TwilioVoice.setSpeakerPhone, + sendDigits: TwilioVoice.sendDigits, + hold: TwilioVoice.hold, requestPermissions(senderId) { if (Platform.OS === ANDROID) { TwilioVoice.requestPermissions(senderId) } }, - getActiveCall() { - return TwilioVoice.getActiveCall() - }, + getActiveCall: TwilioVoice.getActiveCall, + getCallInvite: TwilioVoice.getCallInvite, configureCallKit(params = {}) { if (Platform.OS === IOS) { TwilioVoice.configureCallKit(params) @@ -101,6 +91,10 @@ const Twilio = { } }, addEventListener(type, handler) { + if (!_eventHandlers.hasOwnProperty(type)) { + throw new Error('Event handler not found: ' + type) + } + if (_eventHandlers[type]) if (_eventHandlers[type].has(handler)) { return } @@ -113,6 +107,8 @@ const Twilio = { _eventHandlers[type].get(handler).remove() _eventHandlers[type].delete(handler) } + + } export default Twilio diff --git a/ios/RNTwilioVoice.xcodeproj/project.pbxproj b/ios/RNTwilioVoice.xcodeproj/project.pbxproj index 92ab77a4..a7be7718 100644 --- a/ios/RNTwilioVoice.xcodeproj/project.pbxproj +++ b/ios/RNTwilioVoice.xcodeproj/project.pbxproj @@ -7,26 +7,10 @@ objects = { /* Begin PBXBuildFile section */ - 016DD9B51ECCA23A00315CD4 /* TwilioVoice.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 016DD9B41ECCA23A00315CD4 /* TwilioVoice.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 38F71DFA1E9C333F0067E86F /* RNTwilioVoice.m in Sources */ = {isa = PBXBuildFile; fileRef = 38F71DF91E9C333F0067E86F /* RNTwilioVoice.m */; }; - 38F71DFB1E9C333F0067E86F /* RNTwilioVoice.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 38F71DF81E9C333F0067E86F /* RNTwilioVoice.h */; }; /* End PBXBuildFile section */ -/* Begin PBXCopyFilesBuildPhase section */ - 38F71DF31E9C333F0067E86F /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "include/$(PRODUCT_NAME)"; - dstSubfolderSpec = 16; - files = ( - 38F71DFB1E9C333F0067E86F /* RNTwilioVoice.h in CopyFiles */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - /* Begin PBXFileReference section */ - 016DD9B41ECCA23A00315CD4 /* TwilioVoice.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TwilioVoice.framework; path = "../../../Downloads/twilio-voice-ios/TwilioVoice.framework"; sourceTree = ""; }; 38F71DF51E9C333F0067E86F /* libRNTwilioVoice.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNTwilioVoice.a; sourceTree = BUILT_PRODUCTS_DIR; }; 38F71DF81E9C333F0067E86F /* RNTwilioVoice.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNTwilioVoice.h; sourceTree = ""; }; 38F71DF91E9C333F0067E86F /* RNTwilioVoice.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNTwilioVoice.m; sourceTree = ""; }; @@ -37,7 +21,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 016DD9B51ECCA23A00315CD4 /* TwilioVoice.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -47,7 +30,6 @@ 016DD9801ECC9B4B00315CD4 /* Frameworks */ = { isa = PBXGroup; children = ( - 016DD9B41ECCA23A00315CD4 /* TwilioVoice.framework */, ); name = Frameworks; sourceTree = ""; @@ -86,7 +68,6 @@ buildConfigurationList = 38F71DFE1E9C333F0067E86F /* Build configuration list for PBXNativeTarget "RNTwilioVoice" */; buildPhases = ( 38F71DF11E9C333F0067E86F /* Sources */, - 38F71DF31E9C333F0067E86F /* CopyFiles */, 016DD9B31ECCA22F00315CD4 /* Frameworks */, ); buildRules = ( @@ -118,6 +99,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, ); mainGroup = 38F71DEC1E9C333E0067E86F; @@ -191,7 +173,7 @@ "${SRCROOT}/../../../ios/Pods/Headers/Public", "${SRCROOT}/../../../ios/Pods/Headers/Public/TwilioVoice", ); - IPHONEOS_DEPLOYMENT_TARGET = 10.2; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -241,7 +223,7 @@ "${SRCROOT}/../../../ios/Pods/Headers/Public", "${SRCROOT}/../../../ios/Pods/Headers/Public/TwilioVoice", ); - IPHONEOS_DEPLOYMENT_TARGET = 10.2; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; diff --git a/ios/RNTwilioVoice.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/RNTwilioVoice.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/ios/RNTwilioVoice.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/RNTwilioVoice/RNTwilioVoice.m b/ios/RNTwilioVoice/RNTwilioVoice.m index 0e0a6bfc..c2ad9d2a 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.m +++ b/ios/RNTwilioVoice/RNTwilioVoice.m @@ -10,15 +10,24 @@ @import CallKit; @import TwilioVoice; +NSString * const kCachedDeviceToken = @"CachedDeviceToken"; + @interface RNTwilioVoice () -@property (nonatomic, strong) NSString *deviceTokenString; @property (nonatomic, strong) PKPushRegistry *voipRegistry; +@property (nonatomic, strong) void(^incomingPushCompletionCallback)(void); @property (nonatomic, strong) TVOCallInvite *callInvite; -@property (nonatomic, strong) TVOCall *call; @property (nonatomic, strong) void(^callKitCompletionCallback)(BOOL); +@property (nonatomic, strong) TVODefaultAudioDevice *audioDevice; +@property (nonatomic, strong) NSMutableDictionary *activeCallInvites; +@property (nonatomic, strong) NSMutableDictionary *activeCalls; + +// activeCall represents the last connected call +@property (nonatomic, strong) TVOCall *activeCall; @property (nonatomic, strong) CXProvider *callKitProvider; @property (nonatomic, strong) CXCallController *callKitCallController; +@property (nonatomic, assign) BOOL userInitiatedDisconnect; + @end @implementation RNTwilioVoice { @@ -28,7 +37,6 @@ @implementation RNTwilioVoice { NSString *_token; } -NSString * const StatePending = @"PENDING"; NSString * const StateConnecting = @"CONNECTING"; NSString * const StateConnected = @"CONNECTED"; NSString * const StateDisconnected = @"DISCONNECTED"; @@ -43,7 +51,7 @@ - (dispatch_queue_t)methodQueue - (NSArray *)supportedEvents { - return @[@"connectionDidConnect", @"connectionDidDisconnect", @"callRejected", @"deviceReady", @"deviceNotReady"]; + return @[@"connectionDidConnect", @"connectionDidDisconnect", @"callRejected", @"deviceReady", @"deviceNotReady", @"deviceDidReceiveIncoming", @"callInviteCancelled", @"callStateRinging", @"connectionIsReconnecting", @"connectionDidReconnect"]; } @synthesize bridge = _bridge; @@ -62,14 +70,19 @@ - (void)dealloc { [self initPushRegistry]; } -RCT_EXPORT_METHOD(initWithAccessTokenUrl:(NSString *)tokenUrl) { - _tokenUrl = tokenUrl; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAppTerminateNotification) name:UIApplicationWillTerminateNotification object:nil]; - [self initPushRegistry]; -} - RCT_EXPORT_METHOD(configureCallKit: (NSDictionary *)params) { if (self.callKitCallController == nil) { + /* + * The important thing to remember when providing a TVOAudioDevice is that the device must be set + * before performing any other actions with the SDK (such as connecting a Call, or accepting an incoming Call). + * In this case we've already initialized our own `TVODefaultAudioDevice` instance which we will now set. + */ + self.audioDevice = [TVODefaultAudioDevice audioDevice]; + TwilioVoice.audioDevice = self.audioDevice; + + self.activeCallInvites = [NSMutableDictionary dictionary]; + self.activeCalls = [NSMutableDictionary dictionary]; + _settings = [[NSMutableDictionary alloc] initWithDictionary:params]; CXProviderConfiguration *configuration = [[CXProviderConfiguration alloc] initWithLocalizedName:params[@"appName"]]; configuration.maximumCallGroups = 1; @@ -93,13 +106,11 @@ - (void)dealloc { RCT_EXPORT_METHOD(connect: (NSDictionary *)params) { NSLog(@"Calling phone number %@", [params valueForKey:@"To"]); -// [TwilioVoice setLogLevel:TVOLogLevelVerbose]; - UIDevice* device = [UIDevice currentDevice]; device.proximityMonitoringEnabled = YES; - if (self.call && self.call.state == TVOCallStateConnected) { - [self.call disconnect]; + if (self.activeCall && self.activeCall.state == TVOCallStateConnected) { + [self performEndCallActionWithUUID:self.activeCall.uuid]; } else { NSUUID *uuid = [NSUUID UUID]; NSString *handle = [params valueForKey:@"To"]; @@ -109,86 +120,87 @@ - (void)dealloc { } RCT_EXPORT_METHOD(disconnect) { - NSLog(@"Disconnecting call"); - [self performEndCallActionWithUUID:self.call.uuid]; + NSLog(@"Disconnecting call. UUID %@", self.activeCall.uuid.UUIDString); + self.userInitiatedDisconnect = YES; + [self performEndCallActionWithUUID:self.activeCall.uuid]; } RCT_EXPORT_METHOD(setMuted: (BOOL *)muted) { NSLog(@"Mute/UnMute call"); - self.call.muted = muted; + self.activeCall.muted = muted ? YES : NO; } RCT_EXPORT_METHOD(setSpeakerPhone: (BOOL *)speaker) { - [self toggleAudioRoute:speaker]; + [self toggleAudioRoute: speaker ? YES : NO]; } -RCT_EXPORT_METHOD(sendDigits: (NSString *)digits){ - if (self.call && self.call.state == TVOCallStateConnected) { +RCT_EXPORT_METHOD(sendDigits: (NSString *)digits) { + if (self.activeCall && self.activeCall.state == TVOCallStateConnected) { NSLog(@"SendDigits %@", digits); - [self.call sendDigits:digits]; + [self.activeCall sendDigits:digits]; } } -RCT_EXPORT_METHOD(unregister){ +RCT_EXPORT_METHOD(unregister) { NSLog(@"unregister"); NSString *accessToken = [self fetchAccessToken]; - - [TwilioVoice unregisterWithAccessToken:accessToken - deviceToken:self.deviceTokenString - completion:^(NSError * _Nullable error) { - if (error) { - NSLog(@"An error occurred while unregistering: %@", [error localizedDescription]); - } else { - NSLog(@"Successfully unregistered for VoIP push notifications."); - } - }]; - - self.deviceTokenString = nil; + NSString *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken]; + if ([cachedDeviceToken length] > 0) { + [TwilioVoice unregisterWithAccessToken:accessToken + deviceToken:cachedDeviceToken + completion:^(NSError * _Nullable error) { + if (error) { + NSLog(@"An error occurred while unregistering: %@", [error localizedDescription]); + } else { + NSLog(@"Successfully unregistered for VoIP push notifications."); + } + }]; + } } RCT_REMAP_METHOD(getActiveCall, - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject){ - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - if (self.callInvite) { - if (self.callInvite.callSid){ - [params setObject:self.callInvite.callSid forKey:@"call_sid"]; - } - if (self.callInvite.from){ - [params setObject:self.callInvite.from forKey:@"from"]; - } - if (self.callInvite.to){ - [params setObject:self.callInvite.to forKey:@"to"]; - } - if (self.callInvite.state == TVOCallInviteStatePending) { - [params setObject:StatePending forKey:@"call_state"]; - } else if (self.callInvite.state == TVOCallInviteStateCanceled) { - [params setObject:StateDisconnected forKey:@"call_state"]; - } else if (self.callInvite.state == TVOCallInviteStateRejected) { - [params setObject:StateRejected forKey:@"call_state"]; + activeCallResolver:(RCTPromiseResolveBlock)resolve + activeCallRejecter:(RCTPromiseRejectBlock)reject) { + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (self.activeCall) { + if (self.activeCall.sid) { + [params setObject:self.activeCall.sid forKey:@"call_sid"]; + } + if (self.activeCall.to) { + [params setObject:self.activeCall.to forKey:@"call_to"]; + } + if (self.activeCall.from) { + [params setObject:self.activeCall.from forKey:@"call_from"]; + } + if (self.activeCall.state == TVOCallStateConnected) { + [params setObject:StateConnected forKey:@"call_state"]; + } else if (self.activeCall.state == TVOCallStateConnecting) { + [params setObject:StateConnecting forKey:@"call_state"]; + } else if (self.activeCall.state == TVOCallStateDisconnected) { + [params setObject:StateDisconnected forKey:@"call_state"]; + } } resolve(params); - } else if (self.call) { - if (self.call.sid) { - [params setObject:self.call.sid forKey:@"call_sid"]; - } - if (self.call.to){ - [params setObject:self.call.to forKey:@"call_to"]; - } - if (self.call.from){ - [params setObject:self.call.from forKey:@"call_from"]; - } - if (self.call.state == TVOCallStateConnected) { - [params setObject:StateConnected forKey:@"call_state"]; - } else if (self.call.state == TVOCallStateConnecting) { - [params setObject:StateConnecting forKey:@"call_state"]; - } else if (self.call.state == TVOCallStateDisconnected) { - [params setObject:StateDisconnected forKey:@"call_state"]; +} + +RCT_REMAP_METHOD(getCallInvite, + callInvieteResolver:(RCTPromiseResolveBlock)resolve + callInviteRejecter:(RCTPromiseRejectBlock)reject) { + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (self.activeCallInvites.count) { + // considering only the first call invite + TVOCallInvite *callInvite = [self.activeCallInvites valueForKey:[self.activeCallInvites allKeys][self.activeCallInvites.count-1]]; + if (callInvite.callSid) { + [params setObject:callInvite.callSid forKey:@"call_sid"]; + } + if (callInvite.from) { + [params setObject:callInvite.from forKey:@"call_from"]; + } + if (callInvite.to) { + [params setObject:callInvite.to forKey:@"call_to"]; + } } resolve(params); - } else{ - reject(@"no_call", @"There was no active call", nil); - } } - (void)initPushRegistry { @@ -213,23 +225,42 @@ - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPush NSLog(@"pushRegistry:didUpdatePushCredentials:forType"); if ([type isEqualToString:PKPushTypeVoIP]) { - self.deviceTokenString = [credentials.token description]; + 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]; - - [TwilioVoice registerWithAccessToken:accessToken - deviceToken:self.deviceTokenString - completion:^(NSError *error) { - if (error) { - NSLog(@"An error occurred while registering: %@", [error localizedDescription]); - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - [params setObject:[error localizedDescription] forKey:@"err"]; - - [self sendEventWithName:@"deviceNotReady" body:params]; - } else { - NSLog(@"Successfully registered for VoIP push notifications."); - [self sendEventWithName:@"deviceReady" body:nil]; - } - }]; + NSString *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken]; + if (![cachedDeviceToken isEqualToString:deviceTokenString]) { + cachedDeviceToken = deviceTokenString; + + /* + * Perform registration if a new device token is detected. + */ + [TwilioVoice registerWithAccessToken:accessToken + deviceToken:cachedDeviceToken + completion:^(NSError *error) { + if (error) { + NSLog(@"An error occurred while registering: %@", [error localizedDescription]); + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + [params setObject:[error localizedDescription] forKey:@"err"]; + + [self sendEventWithName:@"deviceNotReady" body:params]; + } + else { + NSLog(@"Successfully registered for VoIP push notifications."); + + /* + * Save the device token after successfully registered. + */ + [[NSUserDefaults standardUserDefaults] setObject:cachedDeviceToken forKey:kCachedDeviceToken]; + [self sendEventWithName:@"deviceReady" body:nil]; + } + }]; + } else { + [self sendEventWithName:@"deviceReady" body:nil]; + } } } @@ -239,8 +270,10 @@ - (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(P if ([type isEqualToString:PKPushTypeVoIP]) { NSString *accessToken = [self fetchAccessToken]; - [TwilioVoice unregisterWithAccessToken:accessToken - deviceToken:self.deviceTokenString + NSString *cachedDeviceToken = [[NSUserDefaults standardUserDefaults] objectForKey:kCachedDeviceToken]; + if ([cachedDeviceToken length] > 0) { + [TwilioVoice unregisterWithAccessToken:accessToken + deviceToken:cachedDeviceToken completion:^(NSError * _Nullable error) { if (error) { NSLog(@"An error occurred while unregistering: %@", [error localizedDescription]); @@ -248,81 +281,183 @@ - (void)pushRegistry:(PKPushRegistry *)registry didInvalidatePushTokenForType:(P NSLog(@"Successfully unregistered for VoIP push notifications."); } }]; - - self.deviceTokenString = nil; + } } } +/** +* Try using the `pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler:` method if +* your application is targeting iOS 11. According to the docs, this delegate method is deprecated by Apple. +*/ - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(NSString *)type { NSLog(@"pushRegistry:didReceiveIncomingPushWithPayload:forType"); - if ([type isEqualToString:PKPushTypeVoIP]) { - [TwilioVoice handleNotification:payload.dictionaryPayload - delegate:self]; + // The Voice SDK will use main queue to invoke `cancelledCallInviteReceived:error` when delegate queue is not passed + if (![TwilioVoice handleNotification:payload.dictionaryPayload delegate:self delegateQueue: nil]) { + NSLog(@"This is not a valid Twilio Voice notification."); + } } } -#pragma mark - TVONotificationDelegate -- (void)callInviteReceived:(TVOCallInvite *)callInvite { - if (callInvite.state == TVOCallInviteStatePending) { - [self handleCallInviteReceived:callInvite]; - } else if (callInvite.state == TVOCallInviteStateCanceled) { - [self handleCallInviteCanceled:callInvite]; - } -} +/** + * This delegate method is available on iOS 11 and above. Call the completion handler once the + * notification payload is passed to the `TwilioVoice.handleNotification()` method. + */ +- (void)pushRegistry:(PKPushRegistry *)registry +didReceiveIncomingPushWithPayload:(PKPushPayload *)payload + forType:(PKPushType)type +withCompletionHandler:(void (^)(void))completion { + NSLog(@"pushRegistry:didReceiveIncomingPushWithPayload:forType:withCompletionHandler"); -- (void)handleCallInviteReceived:(TVOCallInvite *)callInvite { - NSLog(@"callInviteReceived:"); - if (self.callInvite && self.callInvite == TVOCallInviteStatePending) { - NSLog(@"Already a pending incoming call invite."); - NSLog(@" >> Ignoring call from %@", callInvite.from); - return; - } else if (self.call) { - NSLog(@"Already an active call."); - NSLog(@" >> Ignoring call from %@", callInvite.from); - return; - } + // Save for later when the notification is properly handled. + self.incomingPushCompletionCallback = completion; - self.callInvite = callInvite; - [self reportIncomingCallFrom:callInvite.from withUUID:callInvite.uuid]; + if ([type isEqualToString:PKPushTypeVoIP]) { + // The Voice SDK will use main queue to invoke `cancelledCallInviteReceived:error` when delegate queue is not passed + if (![TwilioVoice handleNotification:payload.dictionaryPayload delegate:self delegateQueue: nil]) { + NSLog(@"This is not a valid Twilio Voice notification."); + } + } + if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) { + // Save for later when the notification is properly handled. + self.incomingPushCompletionCallback = completion; + } else { + /** + * The Voice SDK processes the call notification and returns the call invite synchronously. Report the incoming call to + * CallKit and fulfill the completion before exiting this callback method. + */ + completion(); + } } -- (void)handleCallInviteCanceled:(TVOCallInvite *)callInvite { - NSLog(@"callInviteCanceled"); +- (void)incomingPushHandled { + if (self.incomingPushCompletionCallback) { + self.incomingPushCompletionCallback(); + self.incomingPushCompletionCallback = nil; + } +} - [self performEndCallActionWithUUID:callInvite.uuid]; +#pragma mark - TVONotificationDelegate +- (void)callInviteReceived:(TVOCallInvite *)callInvite { + /** + * Calling `[TwilioVoice handleNotification:delegate:]` will synchronously process your notification payload and + * provide you a `TVOCallInvite` object. Report the incoming call to CallKit upon receiving this callback. + */ + NSLog(@"callInviteReceived"); + NSString *from = @"Unknown"; + if (callInvite.from) { + from = [callInvite.from stringByReplacingOccurrencesOfString:@"client:" withString:@""]; + } + // Always report to CallKit + [self reportIncomingCallFrom:from withUUID:callInvite.uuid]; + self.activeCallInvites[[callInvite.uuid UUIDString]] = callInvite; + if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) { + [self incomingPushHandled]; + } - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - if (self.callInvite.callSid){ - [params setObject:self.callInvite.callSid forKey:@"call_sid"]; - } + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (callInvite.callSid) { + [params setObject:callInvite.callSid forKey:@"call_sid"]; + } + if (callInvite.from) { + [params setObject:callInvite.from forKey:@"call_from"]; + } + if (callInvite.to) { + [params setObject:callInvite.to forKey:@"call_to"]; + } + [self sendEventWithName:@"deviceDidReceiveIncoming" body:params]; +} + +- (void)cancelledCallInviteReceived:(nonnull TVOCancelledCallInvite *)cancelledCallInvite { + /** + * The SDK may call `[TVONotificationDelegate callInviteReceived:error:]` asynchronously on the dispatch queue + * with a `TVOCancelledCallInvite` if the caller hangs up or the client encounters any other error before the called + * party could answer or reject the call. + */ + NSLog(@"cancelledCallInviteReceived"); + TVOCallInvite *callInvite; + for (NSString *activeCallInviteId in self.activeCallInvites) { + TVOCallInvite *activeCallInvite = [self.activeCallInvites objectForKey:activeCallInviteId]; + if ([cancelledCallInvite.callSid isEqualToString:activeCallInvite.callSid]) { + callInvite = activeCallInvite; + break; + } + } + if (callInvite) { + [self performEndCallActionWithUUID:callInvite.uuid]; + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (callInvite.callSid) { + [params setObject:callInvite.callSid forKey:@"call_sid"]; + } + if (callInvite.from) { + [params setObject:callInvite.from forKey:@"call_from"]; + } + if (callInvite.to) { + [params setObject:callInvite.to forKey:@"call_to"]; + } + [self sendEventWithName:@"callInviteCancelled" body:params]; + } +} - if (self.callInvite.from){ - [params setObject:self.callInvite.from forKey:@"from"]; - } - if (self.callInvite.to){ - [params setObject:self.callInvite.to forKey:@"to"]; - } - if (self.callInvite.state == TVOCallInviteStateCanceled) { - [params setObject:StateDisconnected forKey:@"call_state"]; - } else if (self.callInvite.state == TVOCallInviteStateRejected) { - [params setObject:StateRejected forKey:@"call_state"]; - } - [self sendEventWithName:@"connectionDidDisconnect" body:params]; - self.callInvite = nil; +- (void)cancelledCallInviteReceived:(TVOCancelledCallInvite *)cancelledCallInvite error:(NSError *)error { + /** + * The SDK may call `[TVONotificationDelegate callInviteReceived:error:]` asynchronously on the dispatch queue + * with a `TVOCancelledCallInvite` if the caller hangs up or the client encounters any other error before the called + * party could answer or reject the call. + */ + NSLog(@"cancelledCallInviteReceived with error"); + TVOCallInvite *callInvite; + for (NSString *activeCallInviteId in self.activeCallInvites) { + TVOCallInvite *activeCallInvite = [self.activeCallInvites objectForKey:activeCallInviteId]; + if ([cancelledCallInvite.callSid isEqualToString:activeCallInvite.callSid]) { + callInvite = activeCallInvite; + break; + } + } + if (callInvite) { + [self performEndCallActionWithUUID:callInvite.uuid]; + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (callInvite.callSid) { + [params setObject:callInvite.callSid forKey:@"call_sid"]; + } + if (callInvite.from) { + [params setObject:callInvite.from forKey:@"call_from"]; + } + if (callInvite.to) { + [params setObject:callInvite.to forKey:@"call_to"]; + } + [self sendEventWithName:@"callInviteCancelled" body:params]; + } } - (void)notificationError:(NSError *)error { NSLog(@"notificationError: %@", [error localizedDescription]); } +#pragma mark - TVOCallDelegate +- (void)callDidStartRinging:(TVOCall *)call { + NSLog(@"callDidStartRinging"); + + /* + When [answerOnBridge](https://www.twilio.com/docs/voice/twiml/dial#answeronbridge) is enabled in the + TwiML verb, the caller will not hear the ringback while the call is ringing and awaiting to be + accepted on the callee's side. The application can use the `AVAudioPlayer` to play custom audio files + between the `[TVOCallDelegate callDidStartRinging:]` and the `[TVOCallDelegate callDidConnect:]` callbacks. + */ + NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; + [callParams setObject:call.sid forKey:@"call_sid"]; + if (call.from) { + [callParams setObject:call.from forKey:@"call_from"]; + } + [self sendEventWithName:@"callStateRinging" body:callParams]; +} + #pragma mark - TVOCallDelegate - (void)callDidConnect:(TVOCall *)call { - self.call = call; + NSLog(@"callDidConnect"); self.callKitCompletionCallback(YES); - self.callKitCompletionCallback = nil; NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; [callParams setObject:call.sid forKey:@"call_sid"]; @@ -332,81 +467,138 @@ - (void)callDidConnect:(TVOCall *)call { [callParams setObject:StateConnected forKey:@"call_state"]; } - if (call.from){ - [callParams setObject:call.from forKey:@"from"]; + if (call.from) { + [callParams setObject:call.from forKey:@"call_from"]; } - if (call.to){ - [callParams setObject:call.to forKey:@"to"]; + if (call.to) { + [callParams setObject:call.to forKey:@"call_to"]; } [self sendEventWithName:@"connectionDidConnect" body:callParams]; } +- (void)call:(TVOCall *)call isReconnectingWithError:(NSError *)error { + NSLog(@"Call is reconnecting"); + NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; + [callParams setObject:call.sid forKey:@"call_sid"]; + if (call.from) { + [callParams setObject:call.from forKey:@"call_from"]; + } + if (call.to) { + [callParams setObject:call.to forKey:@"call_to"]; + } + [self sendEventWithName:@"connectionIsReconnecting" body:callParams]; +} + +- (void)callDidReconnect:(TVOCall *)call { + NSLog(@"Call reconnected"); + NSMutableDictionary *callParams = [[NSMutableDictionary alloc] init]; + [callParams setObject:call.sid forKey:@"call_sid"]; + if (call.from) { + [callParams setObject:call.from forKey:@"call_from"]; + } + if (call.to) { + [callParams setObject:call.to forKey:@"call_to"]; + } + [self sendEventWithName:@"connectionDidReconnect" body:callParams]; +} + - (void)call:(TVOCall *)call didFailToConnectWithError:(NSError *)error { NSLog(@"Call failed to connect: %@", error); self.callKitCompletionCallback(NO); - [self performEndCallActionWithUUID:call.uuid]; - [self callDisconnected:error]; + [self performEndCallActionWithUUID:call.uuid]; + [self callDisconnected:call error:error]; } - (void)call:(TVOCall *)call didDisconnectWithError:(NSError *)error { - NSLog(@"Call disconnected with error: %@", error); + if (error) { + NSLog(@"didDisconnectWithError: %@", error); + } else { + NSLog(@"didDisconnect"); + } - [self performEndCallActionWithUUID:call.uuid]; - [self callDisconnected:error]; -} + UIDevice* device = [UIDevice currentDevice]; + device.proximityMonitoringEnabled = NO; -- (void)callDisconnected:(NSError *)error { - NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - if (error) { - NSString* errMsg = [error localizedDescription]; - if (error.localizedFailureReason) { - errMsg = [error localizedFailureReason]; + if (!self.userInitiatedDisconnect) { + CXCallEndedReason reason = CXCallEndedReasonRemoteEnded; + if (error) { + reason = CXCallEndedReasonFailed; + } + [self.callKitProvider reportCallWithUUID:call.uuid endedAtDate:[NSDate date] reason:reason]; } - [params setObject:errMsg forKey:@"error"]; - } - if (self.call.sid) { - [params setObject:self.call.sid forKey:@"call_sid"]; - } - if (self.call.to){ - [params setObject:self.call.to forKey:@"call_to"]; - } - if (self.call.from){ - [params setObject:self.call.from forKey:@"call_from"]; - } - if (self.call.state == TVOCallStateDisconnected) { - [params setObject:StateDisconnected forKey:@"call_state"]; - } - [self sendEventWithName:@"connectionDidDisconnect" body:params]; - - self.call = nil; - self.callKitCompletionCallback = nil; + [self callDisconnected:call error:error]; } -#pragma mark - AVAudioSession -- (void)toggleAudioRoute: (BOOL *)toSpeaker { - // The mode set by the Voice SDK is "VoiceChat" so the default audio route is the built-in receiver. - // Use port override to switch the route. - NSError *error = nil; - NSLog(@"toggleAudioRoute"); +- (void)callDisconnected:(TVOCall *)call error:(NSError *)error { + NSLog(@"callDisconnect"); + if ([call isEqual:self.activeCall]) { + self.activeCall = nil; + } + [self.activeCalls removeObjectForKey:call.uuid.UUIDString]; + + self.userInitiatedDisconnect = NO; - if (toSpeaker) { - if (![[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker - error:&error]) { - NSLog(@"Unable to reroute audio: %@", [error localizedDescription]); + NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; + if (error) { + NSString* errMsg = [error localizedDescription]; + if (error.localizedFailureReason) { + errMsg = [error localizedFailureReason]; + } + [params setObject:errMsg forKey:@"err"]; } - } else { - if (![[AVAudioSession sharedInstance] overrideOutputAudioPort:AVAudioSessionPortOverrideNone - error:&error]) { - NSLog(@"Unable to reroute audio: %@", [error localizedDescription]); + if (call.sid) { + [params setObject:call.sid forKey:@"call_sid"]; } - } + if (call.to) { + [params setObject:call.to forKey:@"call_to"]; + } + if (call.from) { + [params setObject:call.from forKey:@"call_from"]; + } + if (call.state == TVOCallStateDisconnected) { + [params setObject:StateDisconnected forKey:@"call_state"]; + } + + if (!self.userInitiatedDisconnect) { + CXCallEndedReason reason = CXCallEndedReasonRemoteEnded; + if (error) { + reason = CXCallEndedReasonFailed; + } + [self.callKitProvider reportCallWithUUID:call.uuid endedAtDate:[NSDate date] reason:reason]; + } + + [self sendEventWithName:@"connectionDidDisconnect" body:params]; +} + +#pragma mark - AVAudioSession +- (void)toggleAudioRoute:(BOOL)toSpeaker { + // The mode set by the Voice SDK is "VoiceChat" so the default audio route is the built-in receiver. + // Use port override to switch the route. + self.audioDevice.block = ^ { + // We will execute `kDefaultAVAudioSessionConfigurationBlock` first. + kTVODefaultAVAudioSessionConfigurationBlock(); + + // Overwrite the audio route + AVAudioSession *session = [AVAudioSession sharedInstance]; + NSError *error = nil; + if (toSpeaker) { + if (![session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error]) { + NSLog(@"Unable to reroute audio: %@", [error localizedDescription]); + } + } else { + if (![session overrideOutputAudioPort:AVAudioSessionPortOverrideNone error:&error]) { + NSLog(@"Unable to reroute audio: %@", [error localizedDescription]); + } + } + }; + self.audioDevice.block(); } #pragma mark - CXProviderDelegate - (void)providerDidReset:(CXProvider *)provider { NSLog(@"providerDidReset"); - TwilioVoice.audioEnabled = YES; + self.audioDevice.enabled = YES; } - (void)providerDidBegin:(CXProvider *)provider { @@ -415,12 +607,12 @@ - (void)providerDidBegin:(CXProvider *)provider { - (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession *)audioSession { NSLog(@"provider:didActivateAudioSession"); - TwilioVoice.audioEnabled = YES; + self.audioDevice.enabled = YES; } - (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSession *)audioSession { NSLog(@"provider:didDeactivateAudioSession"); - TwilioVoice.audioEnabled = NO; + self.audioDevice.enabled = NO; } - (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)action { @@ -430,8 +622,8 @@ - (void)provider:(CXProvider *)provider timedOutPerformingAction:(CXAction *)act - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallAction *)action { NSLog(@"provider:performStartCallAction"); - [TwilioVoice configureAudioSession]; - TwilioVoice.audioEnabled = NO; + self.audioDevice.enabled = NO; + self.audioDevice.block(); [self.callKitProvider reportOutgoingCallWithUUID:action.callUUID startedConnectingAtDate:[NSDate date]]; @@ -450,14 +642,8 @@ - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallActio - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action { NSLog(@"provider:performAnswerCallAction"); - // RCP: Workaround from https://forums.developer.apple.com/message/169511 suggests configuring audio in the - // completion block of the `reportNewIncomingCallWithUUID:update:completion:` method instead of in - // `provider:performAnswerCallAction:` per the WWDC examples. - // [TwilioVoice configureAudioSession]; - - NSAssert([self.callInvite.uuid isEqual:action.callUUID], @"We only support one Invite at a time."); - - TwilioVoice.audioEnabled = NO; + self.audioDevice.enabled = NO; + self.audioDevice.block(); [self performAnswerVoiceCallWithUUID:action.callUUID completion:^(BOOL success) { if (success) { [action fulfill]; @@ -472,28 +658,43 @@ - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAct - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action { NSLog(@"provider:performEndCallAction"); - TwilioVoice.audioEnabled = NO; + TVOCallInvite *callInvite = self.activeCallInvites[action.callUUID.UUIDString]; + TVOCall *call = self.activeCalls[action.callUUID.UUIDString]; - if (self.callInvite && self.callInvite.state == TVOCallInviteStatePending) { - [self sendEventWithName:@"callRejected" body:@"callRejected"]; - [self.callInvite reject]; - self.callInvite = nil; - } else if (self.call) { - [self.call disconnect]; - } + if (callInvite) { + [callInvite reject]; + [self sendEventWithName:@"callRejected" body:@"callRejected"]; + [self.activeCallInvites removeObjectForKey:callInvite.uuid.UUIDString]; + } else if (call) { + [call disconnect]; + } else { + NSLog(@"Unknown UUID to perform end-call action with"); + } + self.audioDevice.enabled = YES; [action fulfill]; } - (void)provider:(CXProvider *)provider performSetHeldCallAction:(CXSetHeldCallAction *)action { - if (self.call && self.call.state == TVOCallStateConnected) { - [self.call setOnHold:action.isOnHold]; + TVOCall *call = self.activeCalls[action.callUUID.UUIDString]; + if (call) { + [call setOnHold:action.isOnHold]; [action fulfill]; } else { [action fail]; } } +- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action { + TVOCall *call = self.activeCalls[action.callUUID.UUIDString]; + if (call) { + [call setMuted:action.isMuted]; + [action fulfill]; + } else { + [action fail]; + } +} + #pragma mark - CallKit Actions - (void)performStartCallActionWithUUID:(NSUUID *)uuid handle:(NSString *)handle { if (uuid == nil || handle == nil) { @@ -537,9 +738,6 @@ - (void)reportIncomingCallFrom:(NSString *)from withUUID:(NSUUID *)uuid { [self.callKitProvider reportNewIncomingCallWithUUID:uuid update:callUpdate completion:^(NSError *error) { if (!error) { NSLog(@"Incoming call successfully reported"); - - // RCP: Workaround per https://forums.developer.apple.com/message/169511 - [TwilioVoice configureAudioSession]; } else { NSLog(@"Failed to report incoming call successfully: %@.", [error localizedDescription]); } @@ -551,17 +749,12 @@ - (void)performEndCallActionWithUUID:(NSUUID *)uuid { return; } - UIDevice* device = [UIDevice currentDevice]; - device.proximityMonitoringEnabled = NO; - 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]); - } else { - NSLog(@"EndCallAction transaction request successful"); } }]; } @@ -569,29 +762,52 @@ - (void)performEndCallActionWithUUID:(NSUUID *)uuid { - (void)performVoiceCallWithUUID:(NSUUID *)uuid client:(NSString *)client completion:(void(^)(BOOL success))completionHandler { - - self.call = [TwilioVoice call:[self fetchAccessToken] - params:_callParams - uuid:uuid - delegate:self]; - + __weak typeof(self) weakSelf = self; + TVOConnectOptions *connectOptions = [TVOConnectOptions optionsWithAccessToken:[self fetchAccessToken] block:^(TVOConnectOptionsBuilder *builder) { + __strong typeof(self) strongSelf = weakSelf; + builder.params = strongSelf->_callParams; + builder.uuid = uuid; + }]; + TVOCall *call = [TwilioVoice connectWithOptions:connectOptions delegate:self]; + if (call) { + self.activeCall = call; + self.activeCalls[call.uuid.UUIDString] = call; + } self.callKitCompletionCallback = completionHandler; } - (void)performAnswerVoiceCallWithUUID:(NSUUID *)uuid completion:(void(^)(BOOL success))completionHandler { - self.call = [self.callInvite acceptWithDelegate:self]; - self.callInvite = nil; - self.callKitCompletionCallback = completionHandler; + TVOCallInvite *callInvite = self.activeCallInvites[uuid.UUIDString]; + NSAssert(callInvite, @"No CallInvite matches the UUID"); + TVOAcceptOptions *acceptOptions = [TVOAcceptOptions optionsWithCallInvite:callInvite block:^(TVOAcceptOptionsBuilder *builder) { + builder.uuid = callInvite.uuid; + }]; + + TVOCall *call = [callInvite acceptWithOptions:acceptOptions delegate:self]; + + if (!call) { + completionHandler(NO); + } else { + self.callKitCompletionCallback = completionHandler; + self.activeCall = call; + self.activeCalls[call.uuid.UUIDString] = call; + } + + [self.activeCallInvites removeObjectForKey:callInvite.uuid.UUIDString]; + + if ([[NSProcessInfo processInfo] operatingSystemVersion].majorVersion < 13) { + [self incomingPushHandled]; + } } - (void)handleAppTerminateNotification { NSLog(@"handleAppTerminateNotification called"); - if (self.call) { + if (self.activeCall) { NSLog(@"handleAppTerminateNotification disconnecting an active call"); - [self.call disconnect]; + [self.activeCall disconnect]; } } diff --git a/package.json b/package.json index 02d0d67b..2b6ce29b 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,36 @@ { - "name": "react-native-twilio-programmable-voice", - "version": "3.21.1", - "description": "React Native wrapper for Twilio Programmable Voice SDK", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "_from": "git+https://github.com/X-Guard/react-native-twilio-programmable-voice.git#ea3119b732c59bb0bcca63e10aad7dc2be611ab2", + "_id": "react-native-twilio-programmable-voice@4.0.0", + "_inBundle": false, + "_integrity": "", + "_location": "/@x-guard/react-native-twilio-programmable-voice", + "_phantomChildren": {}, + "_requested": { + "type": "git", + "raw": "@x-guard/react-native-twilio-programmable-voice@git+https://github.com/X-Guard/react-native-twilio-programmable-voice.git#ea3119b732c59bb0bcca63e10aad7dc2be611ab2", + "name": "@x-guard/react-native-twilio-programmable-voice", + "escapedName": "@x-guard%2freact-native-twilio-programmable-voice", + "scope": "@x-guard", + "rawSpec": "git+https://github.com/X-Guard/react-native-twilio-programmable-voice.git#ea3119b732c59bb0bcca63e10aad7dc2be611ab2", + "saveSpec": "git+https://github.com/X-Guard/react-native-twilio-programmable-voice.git#ea3119b732c59bb0bcca63e10aad7dc2be611ab2", + "fetchSpec": "https://github.com/X-Guard/react-native-twilio-programmable-voice.git", + "gitCommittish": "ea3119b732c59bb0bcca63e10aad7dc2be611ab2" + }, + "_requiredBy": [ + "/" + ], + "_resolved": "git+https://github.com/X-Guard/react-native-twilio-programmable-voice.git#ea3119b732c59bb0bcca63e10aad7dc2be611ab2", + "_spec": "@x-guard/react-native-twilio-programmable-voice@git+https://github.com/X-Guard/react-native-twilio-programmable-voice.git#ea3119b732c59bb0bcca63e10aad7dc2be611ab2", + "_where": "/Users/wesdewitte/Documents/projects/xg-rn-alarm-prototype", + "author": { + "name": "Fabrizio Moscon" + }, + "bugs": { + "url": "https://github.com/hoxfon/react-native-twilio-programmable-voice/issues" }, + "bundleDependencies": false, + "deprecated": false, + "description": "React Native wrapper for Twilio Programmable Voice SDK", "devDependencies": {}, "homepage": "https://github.com/hoxfon/react-native-twilio-programmable-voice", "keywords": [ @@ -21,20 +46,15 @@ "gcm", "fcm" ], - "bugs": { - "url": "https://github.com/hoxfon/react-native-twilio-programmable-voice/issues" - }, + "license": "MIT", + "main": "index.js", + "name": "react-native-twilio-programmable-voice", "repository": { "type": "git", - "url": "git+ssh://git@github.com:hoxfon/react-native-twilio-programmable-voice.git" - }, - "rnpm": { - "android": { - "packageInstance": "new TwilioVoicePackage()" - } + "url": "git+ssh://git@github.com/hoxfon/react-native-twilio-programmable-voice.git" }, - "author": { - "name": "Fabrizio Moscon" + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" }, - "license": "MIT" + "version": "4.0.0" } diff --git a/react-native.config.js b/react-native.config.js new file mode 100644 index 00000000..91b625d5 --- /dev/null +++ b/react-native.config.js @@ -0,0 +1,8 @@ +module.exports = { + project: { + ios: {}, + android: { + packageInstance: "new TwilioVoicePackage()" + } + } +};