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
-
+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.
+
+
-[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()"
+ }
+ }
+};