diff --git a/README.md b/README.md
index fc3d469a..1d6a2154 100644
--- a/README.md
+++ b/README.md
@@ -46,6 +46,8 @@ const options = {
alertDescription: 'This application needs to access your phone accounts',
cancelButton: 'Cancel',
okButton: 'ok',
+ imageName: 'phone_account_icon',
+ additionalPermissions: [PermissionsAndroid.PERMISSIONS.example]
}
};
@@ -55,11 +57,17 @@ RNCallKeep.setup(options);
- `options`: Object
- `ios`: object
- `appName`: string (required)
- - It will be displayed on system UI when incoming calls received
+ It will be displayed on system UI when incoming calls received
- `imageName`: string (optional)
- - If provided, it will be displayed on system UI during the call
+ If provided, it will be displayed on system UI during the call
- `ringtoneSound`: string (optional)
- - If provided, it will be played when incoming calls received; the system will use the default ringtone if this is not provided
+ If provided, it will be played when incoming calls received; the system will use the default ringtone if this is not provided
+ - `maximumCallGroups`: string (optional)
+ If provided, the maximum number of call groups supported by this application (Default: 3)
+ - `maximumCallsPerCallGroup`: string (optional)
+ If provided, the maximum number of calls in a single group, used for conferencing (Default: 1, no conferencing)
+ - `supportsVideo`: boolean (optional)
+ If provided, whether or not the application supports video calling (Default: true)
- `android`: object
- `alertTitle`: string (required)
When asking for _phone account_ permission, we need to provider a title for the `Alert` to ask the user for it
@@ -69,13 +77,20 @@ RNCallKeep.setup(options);
Cancel button label
- `okButton`: string (required)
Ok button label
+ - `imageName`: string (optional)
+ The image to use in the Android Phone application's native UI for enabling/disabling calling accounts. Should be a 48x48 HDPI
+ grayscale PNG image. Must be in your drawable resources for the parent application. Must be lowercase and underscore (_) characters
+ only, as Java doesn't like capital letters on resources.
+ - `additionalPermissions`: [PermissionsAndroid] (optional)
+ Any additional permissions you'd like your app to have at first launch. Can be used to simplify permission flows and avoid
+ multiple popups to the user at different times.
## Methods
### setAvailable
_This feature is available only on Android._
-Tell _ConnectionService_ that the device is ready to accept outgoing calls.
+Tell _ConnectionService_ that the device is ready to make outgoing calls.
If not the user will be stuck in the build UI screen without any actions.
Eg: Call it with `false` when disconnected from the sip client, when your token expires ...
@@ -84,21 +99,35 @@ RNCallKeep.setAvailable(true);
```
- `active`: boolean
- - Tell whenever the app is ready or not
+ - Tell whether the app is ready or not
+
+### setCurrentCallActive
+_This feature is available only on Android._
+
+Mark the current call as active (eg: when the callee has answered).
+Necessary to set the correct Android capabilities (hold, mute) once the call is set as active.
+Be sure to set this only after your call is ready for two way audio; used both incoming and outgoing calls.
+
+```js
+RNCallKeep.setCurrentCallActive(uuid);
+```
+
+- `uuid`: string
+ - The `uuid` used for `startCall` or `displayIncomingCall`
### displayIncomingCall
-Display system UI for incoming call
+Display system UI for incoming calls
````js
-RNCallKeep.displayIncomingCall(uuid, handle);
+RNCallKeep.displayIncomingCall(uuid, handle, localizedCallerName);
````
- `uuid`: string
- An `uuid` that should be stored and re-used for `stopCall`.
- `handle`: string
- Phone number of the caller
-- `localizedCallerName`: string (optional, iOS only)
+- `localizedCallerName`: string (optional)
- Name of the caller to be displayed on the native UI
- `handleType`: string (optional, iOS only)
- `generic`
@@ -108,20 +137,39 @@ RNCallKeep.displayIncomingCall(uuid, handle);
- `false` (default)
- `true` (you know... when not false)
+### answerIncomingCall
+_This feature is available only on Android._
+
+Use this to tell the sdk a user answered a call from the app UI.
+
+```js
+RNCallKeep.answerIncomingCall(uuid)
+```
+- `uuid`: string
+ - The `uuid` used for `startCall` or `displayIncomingCall`
+
### startCall
-When you make an outgoing call, tell the device that a call is occurring.
-_This feature is available only on iOS._
+When you make an outgoing call, tell the device that a call is occurring. The argument list is slightly
+different on iOS and Android:
+
+iOS:
+```js
+RNCallKeep.startCall(uuid, handle, contactIdentifier, handleType, hasVideo);
+```
+Android:
```js
-RNCallKeep.startCall(uuid, handle, handleType, hasVideo, contactIdentifier);
+RNCallKeep.startCall(uuid, handle, contactIdentifier);
```
-- _uuid_: string
+- `uuid`: string
- An `uuid` that should be stored and re-used for `stopCall`.
- `handle`: string
- Phone number of the callee
+- `contactIdentifier`: string
+ - The identifier is displayed in the native call UI, and is typically the name of the call recipient.
- `handleType`: string (optional, iOS only)
- `generic`
- `number` (default)
@@ -129,8 +177,23 @@ RNCallKeep.startCall(uuid, handle, handleType, hasVideo, contactIdentifier);
- `hasVideo`: boolean (optional, iOS only)
- `false` (default)
- `true` (you know... when not false)
-- `contactIdentifier`: string (optional)
- - The identifier is displayed in the native call UI, and is typically the name of the call recipient.
+
+
+### updateDisplay
+_This feature is available only on Android._
+
+Sets the Android caller name and number
+Use this to update the Android display after an outgoing call has started
+
+```js
+RNCallKeep.updateDisplay(uuid, localizedCallerName, handle)
+```
+- `uuid`: string
+ - The `uuid` used for `startCall` or `displayIncomingCall`
+- `handle`: string
+ - Phone number of the caller
+- `localizedCallerName`: string (optional)
+ - Name of the caller to be displayed on the native UI
### endCall
@@ -144,19 +207,53 @@ RNCallKeep.endCall(uuid);
- `uuid`: string
- The `uuid` used for `startCall` or `displayIncomingCall`
-### setCurrentCallActive
+### endAllCalls
+
+End all ongoing connections.
+
+```js
+RNCallKeep.endAllCalls();
+```
+
+### rejectCall
-Mark the current call as active (eg: when the callee as answered).
+When you reject an incoming call.
```js
-RNCallKeep.setCurrentCallActive();
+RNCallKeep.rejectCall(uuid);
```
+- `uuid`: string
+ - The `uuid` used for `startCall` or `displayIncomingCall`
+
+### reportEndCallWithUUID
+
+Report that the call ended without the user initiating
+
+```js
+RNCallKeep.reportEndCallWithUUID(uuid, reason);
+```
+
+- `uuid`: string
+ - The `uuid` used for `startCall` or `displayIncomingCall`
+- `reason`: int
+ - Reason for the end call
+ - Call failed: 1
+ - Remote user ended call: 2
+ - Remote user did not answer: 3
+ - `CXCallEndedReason` constants used for iOS. `DisconnectCause` used for Android.
+ - Example enum for reasons
+ ```js
+ END_CALL_REASON = {
+ failed: 1,
+ remoteEnded: 2,
+ unanswered: 3
+ }
+ ```
### setMutedCall
Switch the mic on/off.
-_This feature is available only on iOS._
```js
RNCallKeep.setMutedCall(uuid, true);
@@ -166,6 +263,18 @@ RNCallKeep.setMutedCall(uuid, true);
- uuid of the current call.
- `muted`: boolean
+### setOnHold
+
+Set a call on/off hold.
+
+```js
+RNCallKeep.setOnHold(uuid, true)
+```
+
+- `uuid`: string
+ - uuid of the current call.
+- `hold`: boolean
+
### endAllCalls
End all calls that have been started on the device.
@@ -174,7 +283,6 @@ End all calls that have been started on the device.
RNCallKeep.endAllCalls();
```
-
### checkIfBusy
Checks if there are any active calls on the device and returns a promise with a boolean value (`true` if there're active calls, `false` otherwise).
@@ -239,20 +347,23 @@ RNCallKeep.hasDefaultPhoneAccount(options);
### didReceiveStartCallAction
-User start call action from _Recents_ (Or _Contact_ on Android) in built-in phone app.
-
-Try to start your call action from here (e.g. get credentials of the user by `data.handle` and/or send INVITE to your SIP server)
+Device sends this event once it decides the app is allowed to start a call, either from the built-in phone screens (iOS/_Recents_, Android/_Contact_),
+or by the app calling `RNCallKeep.startCall`.
-After all works are done, remember to call `RNCallKeep.startCall(uuid, calleeNumber)`
+Try to start your app call action from here (e.g. get credentials of the user by `data.handle` and/or send INVITE to your SIP server)
```js
-RNCallKeep.addEventListener('didReceiveStartCallAction', ({ handle }) => {
+RNCallKeep.addEventListener('didReceiveStartCallAction', ({ handle, callUUID, name }) => {
});
```
- `handle` (string)
- - The number/name got from Recents in built-in Phone app
+ - Phone number of the callee
+- `callUUID` (string)
+ - The UUID of the call that is to be answered
+- `name` (string)
+ - Name of the callee
### - answerCall
@@ -265,7 +376,7 @@ RNCallKeep.addEventListener('answerCall', ({ callUUID }) => {
```
- `callUUID` (string)
- - The UUID of the call that is to be answered (iOS only).
+ - The UUID of the call that is to be answered.
### - endCall
@@ -278,7 +389,7 @@ RNCallKeep.addEventListener('endCall', ({ callUUID }) => {
```
- `callUUID` (string)
- - The UUID of the call that is to be answered (iOS only).
+ - The UUID of the call that is to be ended.
### - didActivateAudioSession
@@ -302,7 +413,7 @@ RNCallKeep.addEventListener('didDisplayIncomingCall', ({ error }) => {
});
```
-- `error` (?string)
+- `error` (string)
- iOS only.
### - didPerformSetMutedCallAction
@@ -310,11 +421,15 @@ RNCallKeep.addEventListener('didDisplayIncomingCall', ({ error }) => {
A call was muted by the system or the user:
```js
-RNCallKeep.addEventListener('didPerformSetMutedCallAction', (muted) => {
+RNCallKeep.addEventListener('didPerformSetMutedCallAction', ({ muted, callUUID }) => {
});
-
```
+
+- `muted` (boolean)
+- `callUUID` (string)
+ - The UUID of the call.
+
### - didToggleHoldCallAction
A call was held or unheld by the current user
@@ -327,21 +442,22 @@ RNCallKeep.addEventListener('didToggleHoldCallAction', ({ hold, callUUID }) => {
- `hold` (boolean)
- `callUUID` (string)
- - The UUID of the call that is to be answered (iOS only).
+ - The UUID of the call.
### - didPerformDTMFAction
Used type a number on his dialer
```js
-RNCallKeep.addEventListener('didPerformDTMFAction', ({ dtmf, callUUID }) => {
+RNCallKeep.addEventListener('didPerformDTMFAction', ({ digits, callUUID }) => {
});
```
-- `dtmf` (string)
+- `digits` (string)
+ - The digits that emit the dtmf tone
- `callUUID` (string)
- - iOS only.
+ - The UUID of the call.
## Example
@@ -358,61 +474,98 @@ class RNCallKeepExample extends React.Component {
this.currentCallId = null;
- // Initialise RNCallKeep
+ // Add RNCallKeep Events
+ RNCallKeep.addEventListener('didReceiveStartCallAction', this.didReceiveStartCallAction);
+ RNCallKeep.addEventListener('answerCall', this.onAnswerCallAction);
+ RNCallKeep.addEventListener('endCall', this.onEndCallAction);
+ RNCallKeep.addEventListener('didDisplayIncomingCall', this.onIncomingCallDisplayed);
+ RNCallKeep.addEventListener('didPerformSetMutedCallAction', this.onToggleMute);
+ RNCallKeep.addEventListener('didToggleHoldCallAction', this.onToggleHold);
+ RNCallKeep.addEventListener('didPerformDTMFAction', this.onDTMFAction);
+ RNCallKeep.addEventListener('didActivateAudioSession', this.audioSessionActivated);
+ }
+
+ // Initialise RNCallKeep
+ setup = () => {
const options = {
ios: {
- appName: 'WazoReactNativeDemo',
+ appName: 'ReactNativeWazoDemo',
+ imageName: 'sim_icon',
+ supportsVideo: false,
+ maximumCallGroups: '1',
+ maximumCallsPerCallGroup: '1'
},
android: {
- alertTitle: 'Permissions required',
- alertDescription: 'This application needs to access your phone accounts',
+ alertTitle: 'Permissions Required',
+ alertDescription:
+ 'This application needs to access your phone calling accounts to make calls',
cancelButton: 'Cancel',
okButton: 'ok',
+ imageName: 'sim_icon',
+ additionalPermissions: [PermissionsAndroid.PERMISSIONS.READ_CONTACTS]
}
};
-
try {
RNCallKeep.setup(options);
RNCallKeep.setAvailable(true); // Only used for Android, see doc above.
} catch (err) {
console.error('initializeCallKeep error:', err.message);
}
-
- // Add RNCallKeep Events
- RNCallKeep.addEventListener('didReceiveStartCallAction', this.onNativeCall);
- RNCallKeep.addEventListener('answerCall', this.onAnswerCallAction);
- RNCallKeep.addEventListener('endCall', this.onEndCallAction);
- RNCallKeep.addEventListener('didDisplayIncomingCall', this.onIncomingCallDisplayed);
- RNCallKeep.addEventListener('didPerformSetMutedCallAction', this.onToggleMute);
- RNCallKeep.addEventListener('didActivateAudioSession', this.audioSessionActivated);
}
- onNativeCall = ({ handle }) => {
+ // Use startCall to ask the system to start a call - Initiate an outgoing call from this point
+ startCall = ({ handle, localizedCallerName }) => {
// Your normal start call action
+ RNCallKeep.startCall(this.getCurrentCallId(), handle, localizedCallerName);
+ };
+
+ reportEndCallWithUUID = (callUUID, reason) => {
+ RNCallKeep.reportEndCallWithUUID(callUUID, reason);
+ }
+
+ // Event Listener Callbacks
- RNCallKeep.startCall(this.getCurrentCallId(), handle);
+ didReceiveStartCallAction(data) => {
+ let { handle, callUUID, name } = data;
+ // Get this event after the system decides you can start a call
+ // You can now start a call from within your app
};
- onAnswerCallAction = ({ callUUID }) => {
- // called when the user answer the incoming call
+ onAnswerCallAction = (data) => {
+ let { callUUID } = data;
+ // Called when the user answers an incoming call
};
- onEndCallAction = ({ callUUID }) => {
+ onEndCallAction = (data) => {
+ let { callUUID } = data;
RNCallKeep.endCall(this.getCurrentCallId());
this.currentCallId = null;
};
- onIncomingCallDisplayed = error => {
+ // Currently iOS only
+ onIncomingCallDisplayed = (data) => {
+ let { error } = data;
// You will get this event after RNCallKeep finishes showing incoming call UI
// You can check if there was an error while displaying
};
- onToggleMute = (muted) => {
- // Called when the system or the user mutes a call
+ onToggleMute = (data) => {
+ let { muted, callUUID } = data;
+ // Called when the system or user mutes a call
+ };
+
+ onToggleHold = (data) => {
+ let { hold, callUUID } = data;
+ // Called when the system or user holds a call
};
+ onDTMFAction = (data) => {
+ let { digits, callUUID } = data;
+ // Called when the system or user performs a DTMF action
+ }
+
audioSessionActivated = (data) => {
// you might want to do following things when receiving this event:
// - Start playing ringback if it is an outgoing call
@@ -433,7 +586,7 @@ class RNCallKeepExample extends React.Component {
## Notes
-- On iOS, you should call `setup` each time you want to use callKeep.
+- Call setup once to initiate callkeep.
## Debug
diff --git a/actions.js b/actions.js
index c7dc15be..5e17e8c2 100644
--- a/actions.js
+++ b/actions.js
@@ -7,59 +7,54 @@ const RNCallKeepDidReceiveStartCallAction = 'RNCallKeepDidReceiveStartCallAction
const RNCallKeepPerformAnswerCallAction = 'RNCallKeepPerformAnswerCallAction';
const RNCallKeepPerformEndCallAction = 'RNCallKeepPerformEndCallAction';
const RNCallKeepDidActivateAudioSession = 'RNCallKeepDidActivateAudioSession';
+const RNCallKeepDidDeactivateAudioSession = 'RNCallKeepDidDeactivateAudioSession';
const RNCallKeepDidDisplayIncomingCall = 'RNCallKeepDidDisplayIncomingCall';
const RNCallKeepDidPerformSetMutedCallAction = 'RNCallKeepDidPerformSetMutedCallAction';
const RNCallKeepDidToggleHoldAction = 'RNCallKeepDidToggleHoldAction';
const RNCallKeepDidPerformDTMFAction = 'RNCallKeepDidPerformDTMFAction';
+const RNCallKeepProviderReset = 'RNCallKeepProviderReset';
const isIOS = Platform.OS === 'ios';
-const didReceiveStartCallAction = handler => {
- const listener = eventEmitter.addListener(
- RNCallKeepDidReceiveStartCallAction, (data) => {
- handler(isIOS ? data : { handle: data.number });
- }
- );
-
- if (isIOS) {
- RNCallKeepModule._startCallActionEventListenerAdded();
- }
-
- return listener;
-};
+const didReceiveStartCallAction = handler =>
+ eventEmitter.addListener(RNCallKeepDidReceiveStartCallAction, (data) => handler(data));
const answerCall = handler =>
- eventEmitter.addListener(RNCallKeepPerformAnswerCallAction, (data) => handler(isIOS ? data : {}));
+ eventEmitter.addListener(RNCallKeepPerformAnswerCallAction, (data) => handler(data));
const endCall = handler =>
- eventEmitter.addListener(RNCallKeepPerformEndCallAction, (data) => handler(isIOS ? data : {}));
+ eventEmitter.addListener(RNCallKeepPerformEndCallAction, (data) => handler(data));
const didActivateAudioSession = handler =>
eventEmitter.addListener(RNCallKeepDidActivateAudioSession, handler);
+const didDeactivateAudioSession = handler =>
+ eventEmitter.addListener(RNCallKeepDidDeactivateAudioSession, handler);
+
const didDisplayIncomingCall = handler =>
- eventEmitter.addListener(RNCallKeepDidDisplayIncomingCall, (data) => handler(isIOS ? data.error : null));
+ eventEmitter.addListener(RNCallKeepDidDisplayIncomingCall, (data) => handler(data));
const didPerformSetMutedCallAction = handler =>
- eventEmitter.addListener(RNCallKeepDidPerformSetMutedCallAction, (data) => handler(data.muted));
+ eventEmitter.addListener(RNCallKeepDidPerformSetMutedCallAction, (data) => handler(data));
const didToggleHoldCallAction = handler =>
eventEmitter.addListener(RNCallKeepDidToggleHoldAction, handler);
const didPerformDTMFAction = handler =>
- eventEmitter.addListener(RNCallKeepDidPerformDTMFAction, (data) => {
- const payload = isIOS ? { dtmf: data.digits, callUUID: data.callUUID } : data;
+ eventEmitter.addListener(RNCallKeepDidPerformDTMFAction, (data) => handler(data));
- return handler(payload);
- });
+const didResetProvider = handler =>
+ eventEmitter.addListener(RNCallKeepProviderReset, handler);
export const listeners = {
didReceiveStartCallAction,
answerCall,
endCall,
didActivateAudioSession,
+ didDeactivateAudioSession,
didDisplayIncomingCall,
didPerformSetMutedCallAction,
didToggleHoldCallAction,
didPerformDTMFAction,
+ didResetProvider,
};
diff --git a/android/build.gradle b/android/build.gradle
index fe2c8ef2..eacca2ec 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -20,8 +20,8 @@ android {
buildToolsVersion safeExtGet('buildToolsVersion', "23.0.1")
defaultConfig {
- minSdkVersion safeExtGet('minSdkVersion', 16)
- targetSdkVersion safeExtGet('targetSdkVersion', 22)
+ minSdkVersion safeExtGet('minSdkVersion', 23)
+ targetSdkVersion safeExtGet('targetSdkVersion', 28)
versionCode 1
versionName "1.0"
}
@@ -32,5 +32,5 @@ repositories {
}
dependencies {
- compile 'com.facebook.react:react-native:+'
+ implementation 'com.facebook.react:react-native:+'
}
diff --git a/android/react-native-callkeep.iml b/android/react-native-callkeep.iml
new file mode 100644
index 00000000..65b68864
--- /dev/null
+++ b/android/react-native-callkeep.iml
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ generateDebugSources
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
index 713315cb..98f82ed6 100644
--- a/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
+++ b/android/src/main/java/io/wazo/callkeep/RNCallKeepModule.java
@@ -26,14 +26,19 @@
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Icon;
import android.net.Uri;
import android.util.Log;
import android.os.Build;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
+import android.telecom.CallAudioState;
import android.telecom.Connection;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
@@ -41,13 +46,26 @@
import android.telecom.TelecomManager;
import com.facebook.react.bridge.Arguments;
+import com.facebook.react.bridge.Dynamic;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
+import com.facebook.react.bridge.ReadableArray;
+import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+import static android.support.v4.app.ActivityCompat.requestPermissions;
+
// @see https://github.com/kbagchiGWC/voice-quickstart-android/blob/9a2aff7fbe0d0a5ae9457b48e9ad408740dfb968/exampleConnectionService/src/main/java/com/twilio/voice/examples/connectionservice/VoiceConnectionServiceActivity.java
public class RNCallKeepModule extends ReactContextBaseJavaModule {
public static final int REQUEST_READ_PHONE_STATE = 1337;
@@ -55,6 +73,8 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule {
public static final String CHECKING_PERMS = "CHECKING_PERMS";
public static final String EXTRA_CALLER_NAME = "EXTRA_CALLER_NAME";
+ public static final String EXTRA_CALL_UUID = "EXTRA_CALL_UUID";
+ public static final String EXTRA_CALL_NUMBER = "EXTRA_CALL_NUMBER";
public static final String ACTION_END_CALL = "ACTION_END_CALL";
public static final String ACTION_ANSWER_CALL = "ACTION_ANSWER_CALL";
public static final String ACTION_MUTE_CALL = "ACTION_MUTE_CALL";
@@ -67,22 +87,33 @@ public class RNCallKeepModule extends ReactContextBaseJavaModule {
private static final String E_ACTIVITY_DOES_NOT_EXIST = "E_ACTIVITY_DOES_NOT_EXIST";
private static final String REACT_NATIVE_MODULE_NAME = "RNCallKeep";
- private static final String[] permissions = { Manifest.permission.READ_PHONE_STATE, Manifest.permission.CALL_PHONE };
+ private static final String[] permissions = { Manifest.permission.READ_PHONE_STATE,
+ Manifest.permission.CALL_PHONE, Manifest.permission.RECORD_AUDIO };
private static final String TAG = "RNCallKeepModule";
private static TelecomManager telecomManager;
private static Promise hasPhoneAccountPromise;
private ReactApplicationContext reactContext;
- private static PhoneAccountHandle handle;
+ public static PhoneAccountHandle handle;
private boolean isReceiverRegistered = false;
private VoiceBroadcastReceiver voiceBroadcastReceiver;
+ private ReadableMap _settings;
public RNCallKeepModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
+ }
+
+ @Override
+ public String getName() {
+ return REACT_NATIVE_MODULE_NAME;
+ }
+ @ReactMethod
+ public void setup(ReadableMap options) {
VoiceConnectionService.setAvailable(false);
+ this._settings = options;
if (isConnectionServiceAvailable()) {
this.registerPhoneAccount(this.getAppContext());
@@ -92,22 +123,8 @@ public RNCallKeepModule(ReactApplicationContext reactContext) {
}
}
- public static void onRequestPermissionsResult(int[] grantResults) {
- if ((grantResults.length > 0) && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
- hasPhoneAccountPromise.resolve(hasPhoneAccount());
- return;
- }
-
- hasPhoneAccountPromise.resolve(false);
- }
-
- @Override
- public String getName() {
- return REACT_NATIVE_MODULE_NAME;
- }
-
@ReactMethod
- public void displayIncomingCall(String number, String callerName) {
+ public void displayIncomingCall(String uuid, String number, String callerName) {
if (!isConnectionServiceAvailable() || !hasPhoneAccount()) {
return;
}
@@ -119,12 +136,27 @@ public void displayIncomingCall(String number, String callerName) {
extras.putParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS, uri);
extras.putString(EXTRA_CALLER_NAME, callerName);
+ extras.putString(EXTRA_CALL_UUID, uuid);
telecomManager.addNewIncomingCall(handle, extras);
}
@ReactMethod
- public void startCall(String number, String callerName) {
+ public void answerIncomingCall(String uuid) {
+ if (!isConnectionServiceAvailable() || !hasPhoneAccount()) {
+ return;
+ }
+
+ Connection conn = VoiceConnectionService.getConnection(uuid);
+ if (conn == null) {
+ return;
+ }
+
+ conn.onAnswer();
+ }
+
+ @ReactMethod
+ public void startCall(String uuid, String number, String callerName) {
if (!isConnectionServiceAvailable() || !hasPhoneAccount() || !hasPermissions() || number == null) {
return;
}
@@ -136,6 +168,8 @@ public void startCall(String number, String callerName) {
Bundle callExtras = new Bundle();
callExtras.putString(EXTRA_CALLER_NAME, callerName);
+ callExtras.putString(EXTRA_CALL_UUID, uuid);
+ callExtras.putString(EXTRA_CALL_NUMBER, number);
extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, handle);
extras.putParcelable(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, callExtras);
@@ -144,26 +178,39 @@ public void startCall(String number, String callerName) {
}
@ReactMethod
- public void endCall() {
+ public void endCall(String uuid) {
Log.d(TAG, "endCall called");
if (!isConnectionServiceAvailable() || !hasPhoneAccount()) {
return;
}
- Connection conn = VoiceConnectionService.getConnection();
+ Connection conn = VoiceConnectionService.getConnection(uuid);
if (conn == null) {
return;
}
-
- conn.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
- conn.destroy();
- VoiceConnectionService.deinitConnection();
+ conn.onDisconnect();
Log.d(TAG, "endCall executed");
}
@ReactMethod
- public void checkPhoneAccountPermission(Promise promise) {
+ public void endAllCalls() {
+ Log.d(TAG, "endAllCalls called");
+ if (!isConnectionServiceAvailable() || !hasPhoneAccount()) {
+ return;
+ }
+
+ Map currentConnections = VoiceConnectionService.currentConnections;
+ for (Map.Entry connectionEntry : currentConnections.entrySet()) {
+ Connection connectionToEnd = connectionEntry.getValue();
+ connectionToEnd.onDisconnect();
+ }
+
+ Log.d(TAG, "endAllCalls executed");
+ }
+
+ @ReactMethod
+ public void checkPhoneAccountPermission(ReadableArray optionalPermissions, Promise promise) {
Activity currentActivity = this.getCurrentActivity();
if (!isConnectionServiceAvailable()) {
@@ -174,15 +221,22 @@ public void checkPhoneAccountPermission(Promise promise) {
promise.reject(E_ACTIVITY_DOES_NOT_EXIST, "Activity doesn't exist");
return;
}
+ String[] optionalPermsArr = new String[optionalPermissions.size()];
+ for (int i = 0; i < optionalPermissions.size(); i++) {
+ optionalPermsArr[i] = optionalPermissions.getString(i);
+ }
+
+ String[] allPermissions = Arrays.copyOf(permissions, permissions.length + optionalPermsArr.length);
+ System.arraycopy(optionalPermsArr, 0, allPermissions, permissions.length, optionalPermsArr.length);
hasPhoneAccountPromise = promise;
if (!this.hasPermissions()) {
- ActivityCompat.requestPermissions(currentActivity, permissions, REQUEST_READ_PHONE_STATE);
- return;
+ requestPermissions(currentActivity, allPermissions, REQUEST_READ_PHONE_STATE);
+ return;
}
- promise.resolve(hasPhoneAccount());
+ promise.resolve(!hasPhoneAccount());
}
@ReactMethod
@@ -200,6 +254,87 @@ public void checkDefaultPhoneAccount(Promise promise) {
promise.resolve(telecomManager.getDefaultOutgoingPhoneAccount("tel") != null);
}
+ @ReactMethod
+ public void setOnHold(String uuid, boolean shouldHold) {
+ Connection conn = VoiceConnectionService.getConnection(uuid);
+ if (conn == null) {
+ return;
+ }
+
+ if (shouldHold == true) {
+ conn.onHold();
+ } else {
+ conn.onUnhold();
+ }
+ }
+
+ @ReactMethod
+ public void reportEndCallWithUUID(String uuid, int reason) {
+ if (!isConnectionServiceAvailable() || !hasPhoneAccount()) {
+ return;
+ }
+
+ VoiceConnection conn = (VoiceConnection) VoiceConnectionService.getConnection(uuid);
+ if (conn == null) {
+ return;
+ }
+ conn.reportDisconnect(reason);
+ }
+
+ @ReactMethod
+ public void rejectCall(String uuid) {
+ if (!isConnectionServiceAvailable() || !hasPhoneAccount()) {
+ return;
+ }
+
+ Connection conn = VoiceConnectionService.getConnection(uuid);
+ if (conn == null) {
+ return;
+ }
+
+ conn.onReject();
+ }
+
+ @ReactMethod
+ public void setMutedCall(String uuid, boolean shouldMute) {
+ Connection conn = VoiceConnectionService.getConnection(uuid);
+ if (conn == null) {
+ return;
+ }
+
+ CallAudioState newAudioState = null;
+ //if the requester wants to mute, do that. otherwise unmute
+ if (shouldMute) {
+ newAudioState = new CallAudioState(true, conn.getCallAudioState().getRoute(),
+ conn.getCallAudioState().getSupportedRouteMask());
+ } else {
+ newAudioState = new CallAudioState(false, conn.getCallAudioState().getRoute(),
+ conn.getCallAudioState().getSupportedRouteMask());
+ }
+ conn.onCallAudioStateChanged(newAudioState);
+ }
+
+ @ReactMethod
+ public void sendDTMF(String uuid, String key) {
+ Connection conn = VoiceConnectionService.getConnection(uuid);
+ if (conn == null) {
+ return;
+ }
+ char dtmf = key.charAt(0);
+ conn.onPlayDtmfTone(dtmf);
+ }
+
+ @ReactMethod
+ public void updateDisplay(String uuid, String displayName, String uri) {
+ Connection conn = VoiceConnectionService.getConnection(uuid);
+ if (conn == null) {
+ return;
+ }
+
+ conn.setAddress(Uri.parse(uri), TelecomManager.PRESENTATION_ALLOWED);
+ conn.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED);
+ }
+
@ReactMethod
public void hasPhoneAccount(Promise promise) {
promise.resolve(hasPhoneAccount());
@@ -216,12 +351,13 @@ public void setAvailable(Boolean active) {
}
@ReactMethod
- public void setCurrentCallActive() {
- Connection conn = VoiceConnectionService.getConnection();
+ public void setCurrentCallActive(String uuid) {
+ Connection conn = VoiceConnectionService.getConnection(uuid);
if (conn == null) {
return;
}
+ conn.setConnectionCapabilities(conn.getConnectionCapabilities() | Connection.CAPABILITY_HOLD);
conn.setActive();
}
@@ -283,9 +419,16 @@ private void registerPhoneAccount(Context appContext) {
handle = new PhoneAccountHandle(cName, appName);
- PhoneAccount account = new PhoneAccount.Builder(handle, appName)
- .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
- .build();
+ PhoneAccount.Builder builder = new PhoneAccount.Builder(handle, appName)
+ .setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER);
+
+ if (_settings != null && _settings.hasKey("imageName")) {
+ int identifier = appContext.getResources().getIdentifier(_settings.getString("imageName"), "drawable", appContext.getPackageName());
+ Icon icon = Icon.createWithResource(appContext, identifier);
+ builder.setIcon(icon);
+ }
+
+ PhoneAccount account = builder.build();
telecomManager = (TelecomManager) this.getAppContext().getSystemService(this.getAppContext().TELECOM_SERVICE);
telecomManager.registerPhoneAccount(account);
@@ -317,7 +460,7 @@ private Boolean hasPermissions() {
}
private static boolean hasPhoneAccount() {
- return !isConnectionServiceAvailable() ? false : telecomManager.getPhoneAccount(handle).isEnabled();
+ return isConnectionServiceAvailable() && telecomManager.getPhoneAccount(handle).isEnabled();
}
private void registerReceiver() {
@@ -341,44 +484,66 @@ private Context getAppContext() {
return this.reactContext.getApplicationContext();
}
+ public static void onRequestPermissionsResult(int requestCode, String[] grantedPermissions, int[] grantResults) {
+ int permissionsIndex = 0;
+ List permsList = Arrays.asList(permissions);
+ for (int result : grantResults) {
+ if (permsList.contains(grantedPermissions[permissionsIndex]) && result != PackageManager.PERMISSION_GRANTED) {
+ hasPhoneAccountPromise.resolve(false);
+ return;
+ }
+ permissionsIndex++;
+ }
+ hasPhoneAccountPromise.resolve(true);
+ }
+
private class VoiceBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
WritableMap args = Arguments.createMap();
+ HashMap attributeMap = (HashMap)intent.getSerializableExtra("attributeMap");
+ if (attributeMap == null) {
+ return;
+ }
switch (intent.getAction()) {
case ACTION_END_CALL:
- sendEventToJS("RNCallKeepPerformEndCallAction", null);
+ args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID));
+ sendEventToJS("RNCallKeepPerformEndCallAction", args);
break;
case ACTION_ANSWER_CALL:
- sendEventToJS("RNCallKeepPerformAnswerCallAction", null);
+ args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID));
+ sendEventToJS("RNCallKeepPerformAnswerCallAction", args);
break;
case ACTION_HOLD_CALL:
args.putBoolean("hold", true);
+ args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID));
sendEventToJS("RNCallKeepDidToggleHoldAction", args);
break;
case ACTION_UNHOLD_CALL:
args.putBoolean("hold", false);
+ args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID));
sendEventToJS("RNCallKeepDidToggleHoldAction", args);
break;
case ACTION_MUTE_CALL:
args.putBoolean("muted", true);
-
+ args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID));
sendEventToJS("RNCallKeepDidPerformSetMutedCallAction", args);
break;
case ACTION_UNMUTE_CALL:
args.putBoolean("muted", false);
-
+ args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID));
sendEventToJS("RNCallKeepDidPerformSetMutedCallAction", args);
break;
case ACTION_DTMF_TONE:
- args.putString("dtmf", intent.getStringExtra("attribute"));
-
+ args.putString("digits", attributeMap.get("DTMF"));
+ args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID));
sendEventToJS("RNCallKeepDidPerformDTMFAction", args);
break;
case ACTION_ONGOING_CALL:
- args.putString("number", intent.getStringExtra("attribute"));
-
+ args.putString("handle", attributeMap.get(EXTRA_CALL_NUMBER));
+ args.putString("callUUID", attributeMap.get(EXTRA_CALL_UUID));
+ args.putString("name", attributeMap.get(EXTRA_CALLER_NAME));
sendEventToJS("RNCallKeepDidReceiveStartCallAction", args);
break;
case ACTION_AUDIO_SESSION:
diff --git a/android/src/main/java/io/wazo/callkeep/VoiceConference.java b/android/src/main/java/io/wazo/callkeep/VoiceConference.java
new file mode 100644
index 00000000..14742283
--- /dev/null
+++ b/android/src/main/java/io/wazo/callkeep/VoiceConference.java
@@ -0,0 +1,44 @@
+package io.wazo.callkeep;
+
+import android.telecom.Conference;
+import android.telecom.Connection;
+import android.telecom.PhoneAccountHandle;
+
+public class VoiceConference extends Conference {
+
+ VoiceConference(PhoneAccountHandle phoneAccountHandle) {
+ super(phoneAccountHandle);
+ this.setActive();
+// this.setConnectionCapabilities(Connection.CAPABILITY_MUTE | Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD);
+ }
+
+ @Override
+ public void onMerge() {
+ super.onMerge();
+ }
+
+ @Override
+ public void onSeparate(Connection connection) {
+ super.onSeparate(connection);
+ }
+
+ @Override
+ public void onDisconnect() {
+ super.onDisconnect();
+ }
+
+ @Override
+ public void onConnectionAdded(Connection connection) {
+ super.onConnectionAdded(connection);
+ }
+
+ @Override
+ public void onHold() {
+ super.onHold();
+ }
+
+ @Override
+ public void onUnhold() {
+ super.onUnhold();
+ }
+}
diff --git a/android/src/main/java/io/wazo/callkeep/VoiceConnection.java b/android/src/main/java/io/wazo/callkeep/VoiceConnection.java
new file mode 100644
index 00000000..a91db7a6
--- /dev/null
+++ b/android/src/main/java/io/wazo/callkeep/VoiceConnection.java
@@ -0,0 +1,195 @@
+package io.wazo.callkeep;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.Nullable;
+import android.support.v4.content.LocalBroadcastManager;
+import android.telecom.CallAudioState;
+import android.telecom.Connection;
+import android.telecom.DisconnectCause;
+import android.telecom.TelecomManager;
+import android.net.Uri;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.HashMap;
+
+import static io.wazo.callkeep.RNCallKeepModule.ACTION_ANSWER_CALL;
+import static io.wazo.callkeep.RNCallKeepModule.ACTION_AUDIO_SESSION;
+import static io.wazo.callkeep.RNCallKeepModule.ACTION_DTMF_TONE;
+import static io.wazo.callkeep.RNCallKeepModule.ACTION_END_CALL;
+import static io.wazo.callkeep.RNCallKeepModule.ACTION_HOLD_CALL;
+import static io.wazo.callkeep.RNCallKeepModule.ACTION_MUTE_CALL;
+import static io.wazo.callkeep.RNCallKeepModule.ACTION_UNHOLD_CALL;
+import static io.wazo.callkeep.RNCallKeepModule.ACTION_UNMUTE_CALL;
+import static io.wazo.callkeep.RNCallKeepModule.EXTRA_CALLER_NAME;
+import static io.wazo.callkeep.RNCallKeepModule.EXTRA_CALL_NUMBER;
+import static io.wazo.callkeep.RNCallKeepModule.EXTRA_CALL_UUID;
+
+@TargetApi(Build.VERSION_CODES.M)
+public class VoiceConnection extends Connection {
+ private boolean isMuted = false;
+ private HashMap handle;
+ private Context context;
+ private static final String TAG = "RNCK:VoiceConnection";
+
+ VoiceConnection(Context context, HashMap handle) {
+ super();
+ this.handle = handle;
+ this.context = context;
+
+ String number = handle.get(EXTRA_CALL_NUMBER);
+ String name = handle.get(EXTRA_CALLER_NAME);
+
+ if (number != null) {
+ setAddress(Uri.parse(number), TelecomManager.PRESENTATION_ALLOWED);
+ }
+ if (name != null && !name.equals("")) {
+ setCallerDisplayName(name, TelecomManager.PRESENTATION_ALLOWED);
+ }
+ }
+
+ @Override
+ public void onExtrasChanged(Bundle extras) {
+ super.onExtrasChanged(extras);
+ HashMap attributeMap = (HashMap)extras.getSerializable("attributeMap");
+ if (attributeMap != null) {
+ handle = attributeMap;
+ }
+ }
+
+ @Override
+ public void onCallAudioStateChanged(CallAudioState state) {
+ if (state.isMuted() == this.isMuted) {
+ return;
+ }
+
+ this.isMuted = state.isMuted();
+ sendCallRequestToActivity(isMuted ? ACTION_MUTE_CALL : ACTION_UNMUTE_CALL, handle);
+ }
+
+ @Override
+ public void onAnswer() {
+ super.onAnswer();
+ Log.d(TAG, "onAnswer called");
+
+ setConnectionCapabilities(getConnectionCapabilities() | Connection.CAPABILITY_HOLD);
+ setAudioModeIsVoip(true);
+
+ sendCallRequestToActivity(ACTION_ANSWER_CALL, handle);
+ sendCallRequestToActivity(ACTION_AUDIO_SESSION, null);
+ Log.d(TAG, "onAnswer executed");
+ }
+
+ @Override
+ public void onPlayDtmfTone(char dtmf) {
+ try {
+ handle.put("DTMF", Character.toString(dtmf));
+ } catch (Throwable exception) {
+ Log.e(TAG, "Handle map error", exception);
+ }
+ sendCallRequestToActivity(ACTION_DTMF_TONE, handle);
+ }
+
+ @Override
+ public void onDisconnect() {
+ super.onDisconnect();
+ setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
+ sendCallRequestToActivity(ACTION_END_CALL, handle);
+ Log.d(TAG, "onDisconnect executed");
+ try {
+ ((VoiceConnectionService) context).deinitConnection(handle.get(EXTRA_CALL_UUID));
+ } catch(Throwable exception) {
+ Log.e(TAG, "Handle map error", exception);
+ }
+ destroy();
+ }
+
+ public void reportDisconnect(int reason) {
+ super.onDisconnect();
+ switch (reason) {
+ case 1:
+ setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
+ break;
+ case 2:
+ setDisconnected(new DisconnectCause(DisconnectCause.REMOTE));
+ break;
+ case 3:
+ setDisconnected(new DisconnectCause(DisconnectCause.BUSY));
+ break;
+ default:
+ break;
+ }
+ ((VoiceConnectionService)context).deinitConnection(handle.get(EXTRA_CALL_UUID));
+ destroy();
+ }
+
+ @Override
+ public void onAbort() {
+ super.onAbort();
+ setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
+ sendCallRequestToActivity(ACTION_END_CALL, handle);
+ Log.d(TAG, "onAbort executed");
+ try {
+ ((VoiceConnectionService) context).deinitConnection(handle.get(EXTRA_CALL_UUID));
+ } catch(Throwable exception) {
+ Log.e(TAG, "Handle map error", exception);
+ }
+ destroy();
+ }
+
+ @Override
+ public void onHold() {
+ super.onHold();
+ this.setOnHold();
+ sendCallRequestToActivity(ACTION_HOLD_CALL, handle);
+ }
+
+ @Override
+ public void onUnhold() {
+ super.onUnhold();
+ sendCallRequestToActivity(ACTION_UNHOLD_CALL, handle);
+ setActive();
+ }
+
+ @Override
+ public void onReject() {
+ super.onReject();
+ setDisconnected(new DisconnectCause(DisconnectCause.REJECTED));
+ sendCallRequestToActivity(ACTION_END_CALL, handle);
+ Log.d(TAG, "onReject executed");
+ try {
+ ((VoiceConnectionService) context).deinitConnection(handle.get(EXTRA_CALL_UUID));
+ } catch(Throwable exception) {
+ Log.e(TAG, "Handle map error", exception);
+ }
+ destroy();
+ }
+
+ /*
+ * Send call request to the RNCallKeepModule
+ */
+ private void sendCallRequestToActivity(final String action, @Nullable final HashMap attributeMap) {
+ final VoiceConnection instance = this;
+ final Handler handler = new Handler();
+
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ Intent intent = new Intent(action);
+ if (attributeMap != null) {
+ Bundle extras = new Bundle();
+ extras.putSerializable("attributeMap", attributeMap);
+ intent.putExtras(extras);
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
+ }
+ }
+ });
+ }
+}
diff --git a/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java b/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java
index eb40cd6b..e7969308 100644
--- a/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java
+++ b/android/src/main/java/io/wazo/callkeep/VoiceConnectionService.java
@@ -19,9 +19,11 @@
import android.annotation.TargetApi;
import android.content.Intent;
+import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
+import android.speech.tts.Voice;
import android.support.annotation.Nullable;
import android.support.v4.content.LocalBroadcastManager;
import android.telecom.CallAudioState;
@@ -33,6 +35,15 @@
import android.telecom.TelecomManager;
import android.util.Log;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
import static io.wazo.callkeep.RNCallKeepModule.ACTION_ANSWER_CALL;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_AUDIO_SESSION;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_DTMF_TONE;
@@ -43,16 +54,27 @@
import static io.wazo.callkeep.RNCallKeepModule.ACTION_UNHOLD_CALL;
import static io.wazo.callkeep.RNCallKeepModule.ACTION_UNMUTE_CALL;
import static io.wazo.callkeep.RNCallKeepModule.EXTRA_CALLER_NAME;
+import static io.wazo.callkeep.RNCallKeepModule.EXTRA_CALL_NUMBER;
+import static io.wazo.callkeep.RNCallKeepModule.EXTRA_CALL_UUID;
+import static io.wazo.callkeep.RNCallKeepModule.handle;
// @see https://github.com/kbagchiGWC/voice-quickstart-android/blob/9a2aff7fbe0d0a5ae9457b48e9ad408740dfb968/exampleConnectionService/src/main/java/com/twilio/voice/examples/connectionservice/VoiceConnectionService.java
@TargetApi(Build.VERSION_CODES.M)
public class VoiceConnectionService extends ConnectionService {
- private static Connection connection;
private static Boolean isAvailable = false;
- private static final String TAG = "RNCallKeepModule";
+ private static String TAG = "RNCK:VoiceConnectionService";
+ public static Map currentConnections = new HashMap<>();
- public static Connection getConnection() {
- return connection;
+ public static Connection getConnection(String connectionId) {
+ if (currentConnections.containsKey(connectionId)) {
+ return currentConnections.get(connectionId);
+ }
+ return null;
+ }
+
+ public VoiceConnectionService() {
+ super();
+ Log.e(TAG, "Constructor");
}
public static void setAvailable(Boolean value) {
@@ -60,15 +82,21 @@ public static void setAvailable(Boolean value) {
}
- public static void deinitConnection() {
- Log.d(TAG, "deinitConnection");
- connection = null;
+ public static void deinitConnection(String connectionId) {
+ Log.d(TAG, "deinitConnection:" + connectionId);
+ if (currentConnections.containsKey(connectionId)) {
+ currentConnections.remove(connectionId);
+ }
}
@Override
public Connection onCreateIncomingConnection(PhoneAccountHandle connectionManagerPhoneAccount, ConnectionRequest request) {
+ Bundle extra = request.getExtras();
+ Uri number = request.getAddress();
+ String name = extra.getString(EXTRA_CALLER_NAME);
Connection incomingCallConnection = createConnection(request);
incomingCallConnection.setRinging();
+ incomingCallConnection.setInitialized();
return incomingCallConnection;
}
@@ -78,12 +106,31 @@ public Connection onCreateOutgoingConnection(PhoneAccountHandle connectionManage
if (!this.canMakeOutgoingCall()) {
return Connection.createFailedConnection(new DisconnectCause(DisconnectCause.LOCAL));
}
-
- Connection outgoingCallConnection = createConnection(request);
+ // TODO: Hold all other calls
+
+ Bundle extras = request.getExtras();
+ Connection outgoingCallConnection = null;
+ String number = request.getAddress().getSchemeSpecificPart();
+ String extrasNumber = extras.getString(EXTRA_CALL_NUMBER);
+ String name = extras.getString(EXTRA_CALLER_NAME);
+
+ if (extrasNumber != null && extrasNumber.equals(number)) {
+ outgoingCallConnection = createConnection(request);
+ } else {
+ String uuid = UUID.randomUUID().toString();
+ extras.putString(EXTRA_CALL_UUID, uuid);
+ extras.putString(EXTRA_CALLER_NAME, name);
+ extras.putString(EXTRA_CALL_NUMBER, number);
+ outgoingCallConnection = createConnection(request);
+ }
outgoingCallConnection.setDialing();
outgoingCallConnection.setAudioModeIsVoip(true);
+ outgoingCallConnection.setCallerDisplayName(name, TelecomManager.PRESENTATION_ALLOWED);
+ outgoingCallConnection.setInitialized();
+
+ HashMap extrasMap = this.bundleToMap(extras);
- sendCallRequestToActivity(ACTION_ONGOING_CALL, request.getAddress().getSchemeSpecificPart());
+ sendCallRequestToActivity(ACTION_ONGOING_CALL, extrasMap);
sendCallRequestToActivity(ACTION_AUDIO_SESSION, null);
return outgoingCallConnection;
@@ -94,119 +141,51 @@ private Boolean canMakeOutgoingCall() {
}
private Connection createConnection(ConnectionRequest request) {
- connection = new Connection() {
- private boolean isMuted = false;
-
- @Override
- public void onCallAudioStateChanged(CallAudioState state) {
- if (state.isMuted() == this.isMuted) {
- return;
- }
-
- this.isMuted = state.isMuted();
-
- sendCallRequestToActivity(isMuted ? ACTION_MUTE_CALL : ACTION_UNMUTE_CALL, null);
- }
-
- @Override
- public void onAnswer() {
- super.onAnswer();
- Log.d(TAG, "onAnswer called");
- if (connection == null) {
- return;
- }
-
- connection.setActive();
- connection.setAudioModeIsVoip(true);
-
- sendCallRequestToActivity(ACTION_ANSWER_CALL, null);
- sendCallRequestToActivity(ACTION_AUDIO_SESSION, null);
- Log.d(TAG, "onAnswer executed");
- }
-
- @Override
- public void onPlayDtmfTone(char dtmf) {
- sendCallRequestToActivity(ACTION_DTMF_TONE, String.valueOf(dtmf));
- }
-
- @Override
- public void onDisconnect() {
- super.onDisconnect();
- Log.d(TAG, "onDisconnect called");
- if (connection == null) {
- return;
- }
-
-
- connection.setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
- connection.destroy();
- connection = null;
- sendCallRequestToActivity(ACTION_END_CALL, null);
- Log.d(TAG, "onDisconnect executed");
- }
-
- @Override
- public void onAbort() {
- super.onAbort();
- Log.d(TAG, "onAbort called");
- if (connection == null) {
- return;
- }
-
- connection.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
- connection.destroy();
-
- sendCallRequestToActivity(ACTION_END_CALL, null);
- Log.d(TAG, "onAbort executed");
- }
-
- @Override
- public void onHold() {
- super.onHold();
- connection.setOnHold();
-
- sendCallRequestToActivity(ACTION_HOLD_CALL, null);
- }
-
- @Override
- public void onUnhold() {
- super.onUnhold();
- connection.setActive();
-
- sendCallRequestToActivity(ACTION_UNHOLD_CALL, null);
+ Bundle extras = request.getExtras();
+ HashMap extrasMap = this.bundleToMap(extras);
+ extrasMap.put(EXTRA_CALL_NUMBER, request.getAddress().toString());
+ VoiceConnection connection = new VoiceConnection(this, extrasMap);
+ connection.setConnectionCapabilities(Connection.CAPABILITY_MUTE | Connection.CAPABILITY_SUPPORT_HOLD);
+ connection.setInitializing();
+ connection.setExtras(extras);
+ currentConnections.put(extras.getString(EXTRA_CALL_UUID), connection);
+
+ // Get other connections for conferencing
+ Map otherConnections = new HashMap<>();
+ for (Map.Entry entry : currentConnections.entrySet()) {
+ if(!(extras.getString(EXTRA_CALL_UUID).equals(entry.getKey()))) {
+ otherConnections.put(entry.getKey(), entry.getValue());
}
+ }
+ List conferenceConnections = new ArrayList(otherConnections.values());
+ connection.setConferenceableConnections(conferenceConnections);
- @Override
- public void onReject() {
- super.onReject();
- Log.d(TAG, "onReject called");
- if (connection == null) {
- return;
- }
+ return connection;
+ }
- connection.setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
- connection.destroy();
+ @Override
+ public void onConference(Connection connection1, Connection connection2) {
+ super.onConference(connection1, connection2);
+ VoiceConnection voiceConnection1 = (VoiceConnection) connection1;
+ VoiceConnection voiceConnection2 = (VoiceConnection) connection2;
- sendCallRequestToActivity(ACTION_END_CALL, null);
- Log.d(TAG, "onReject executed");
- }
- };
+ PhoneAccountHandle phoneAccountHandle = RNCallKeepModule.handle;
- Bundle extra = request.getExtras();
+ VoiceConference voiceConference = new VoiceConference(handle);
+ voiceConference.addConnection(voiceConnection1);
+ voiceConference.addConnection(voiceConnection2);
- connection.setConnectionCapabilities(Connection.CAPABILITY_MUTE | Connection.CAPABILITY_HOLD | Connection.CAPABILITY_SUPPORT_HOLD);
- connection.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED);
- connection.setExtras(extra);
- connection.setCallerDisplayName(extra.getString(EXTRA_CALLER_NAME), TelecomManager.PRESENTATION_ALLOWED);
+ connection1.onUnhold();
+ connection2.onUnhold();
- return connection;
+ this.addConference(voiceConference);
}
/*
* Send call request to the RNCallKeepModule
*/
- private void sendCallRequestToActivity(final String action, @Nullable final String attribute) {
+ private void sendCallRequestToActivity(final String action, @Nullable final HashMap attributeMap) {
final VoiceConnectionService instance = this;
final Handler handler = new Handler();
@@ -214,12 +193,27 @@ private void sendCallRequestToActivity(final String action, @Nullable final Stri
@Override
public void run() {
Intent intent = new Intent(action);
- if (attribute != null) {
- intent.putExtra("attribute", attribute);
+ if (attributeMap != null) {
+ Bundle extras = new Bundle();
+ extras.putSerializable("attributeMap", attributeMap);
+ intent.putExtras(extras);
+ LocalBroadcastManager.getInstance(instance).sendBroadcast(intent);
}
-
- LocalBroadcastManager.getInstance(instance).sendBroadcast(intent);
}
});
}
+
+ private HashMap bundleToMap(Bundle extras) {
+ HashMap extrasMap = new HashMap<>();
+ Set keySet = extras.keySet();
+ Iterator iterator = keySet.iterator();
+
+ while(iterator.hasNext()) {
+ String key = iterator.next();
+ if (extras.get(key) != null) {
+ extrasMap.put(key, extras.get(key).toString());
+ }
+ }
+ return extrasMap;
+ }
}
diff --git a/docs/android-installation.md b/docs/android-installation.md
index 4395f92d..4ec5c588 100644
--- a/docs/android-installation.md
+++ b/docs/android-installation.md
@@ -22,7 +22,7 @@ project(':react-native-callkeep').projectDir = new File(rootProject.projectDir,
3. In `android/app/src/main/java/.../MainApplication.java`:
```java
-import io.wazo.callkeep.RNCallKeepPackage; // Add this import line with others
+import io.wazo.callkeep.RNCallKeepPackage; // Add this import line
//...
private static List getPackages() {
@@ -36,18 +36,20 @@ private static List getPackages() {
4. Add permissionResult listener in `MainActivity.java`:
```java
-import io.wazo.callkeep.RNCallKeepModule; // Add this import line with others
+import io.wazo.callkeep.RNCallKeepModule; // Add these import lines
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
public class MainActivity extends ReactActivity {
// ...
// Permission results
@Override
- public void onRequestPermissionsResult(int permsRequestCode, String[] permissions, int[] grantResults) {
- super.onRequestPermissionsResult(permsRequestCode, permissions, grantResults);
- switch (permsRequestCode) {
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ switch (requestCode) {
case RNCallKeepModule.REQUEST_READ_PHONE_STATE:
- RNCallKeepModule.onRequestPermissionsResult(grantResults);
+ RNCallKeepModule.onRequestPermissionsResult(requestCode, permissions, grantResults);
break;
}
}
diff --git a/index.d.ts b/index.d.ts
index ee493160..bc270924 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -1,4 +1,5 @@
-export type Events = 'didReceiveStartCallAction' |
+export type Events =
+ 'didReceiveStartCallAction' |
'answerCall' |
'endCall' |
'didActivateAudioSession' |
@@ -99,6 +100,13 @@ export default class RNCallKeep {
}
+ /**
+ * @descriptions sendDTMF is used to send DTMF tones to the PBX.
+ */
+ static sendDTMF(uuid: string, key: string) {
+
+ }
+
/**
* @description setMutedCall method is available only on iOS.
*/
diff --git a/index.js b/index.js
index dca29dcb..575dc1fd 100644
--- a/index.js
+++ b/index.js
@@ -47,32 +47,64 @@ class RNCallKeep {
displayIncomingCall = (uuid, handle, localizedCallerName, handleType = 'number', hasVideo = false) => {
if (!isIOS) {
- RNCallKeepModule.displayIncomingCall(handle, localizedCallerName);
+ RNCallKeepModule.displayIncomingCall(uuid, handle, localizedCallerName);
return;
}
RNCallKeepModule.displayIncomingCall(uuid, handle, handleType, hasVideo, localizedCallerName);
};
- startCall = (uuid, handle, handleType = 'number', hasVideo = false, contactIdentifier) => {
+ answerIncomingCall = (uuid) => {
if (!isIOS) {
- RNCallKeepModule.startCall(handle, contactIdentifier);
+ RNCallKeepModule.answerIncomingCall(uuid);
+ }
+ }
+
+ startCall = (uuid, handle, contactIdentifier, handleType = 'number', hasVideo = false ) => {
+ if (!isIOS) {
+ RNCallKeepModule.startCall(uuid, handle, contactIdentifier);
return;
}
- RNCallKeepModule.startCall(uuid, handle, handleType, hasVideo, contactIdentifier);
+ RNCallKeepModule.startCall(uuid, handle, contactIdentifier, handleType, hasVideo);
+ };
+
+ reportConnectingOutgoingCallWithUUID = (uuid) => {
+ //only available on iOS
+ if (isIOS) {
+ RNCallKeepModule.reportConnectingOutgoingCallWithUUID(uuid);
+ }
};
reportConnectedOutgoingCallWithUUID = (uuid) => {
- RNCallKeepModule.reportConnectedOutgoingCallWithUUID(uuid);
+ //only available on iOS
+ if (isIOS) {
+ RNCallKeepModule.reportConnectedOutgoingCallWithUUID(uuid);
+ }
};
+ reportEndCallWithUUID = (uuid, reason) => {
+ RNCallKeepModule.reportEndCallWithUUID(uuid, reason);
+ }
+
+ /*
+ * Android explicitly states we reject a call
+ * On iOS we just notify of an endCall
+ */
+ rejectCall = (uuid) => {
+ if (!isIOS) {
+ RNCallKeepModule.rejectCall(uuid);
+ } else {
+ RNCallKeepModule.endCall(uuid);
+ }
+ }
+
endCall = (uuid) => {
- isIOS ? RNCallKeepModule.endCall(uuid) : RNCallKeepModule.endCall();
+ RNCallKeepModule.endCall(uuid);
};
endAllCalls = () => {
- isIOS ? RNCallKeepModule.endAllCalls() : RNCallKeepModule.endCall();
+ RNCallKeepModule.endAllCalls();
};
supportConnectionService = () => supportConnectionService;
@@ -80,15 +112,14 @@ class RNCallKeep {
hasPhoneAccount = async () =>
isIOS ? true : await RNCallKeepModule.hasPhoneAccount();
- setMutedCall = (uuid, muted) => {
- if (!isIOS) {
- // Can't mute on Android
- return;
- }
-
- RNCallKeepModule.setMutedCall(uuid, muted);
+ setMutedCall = (uuid, shouldMute) => {
+ RNCallKeepModule.setMutedCall(uuid, shouldMute);
};
+ sendDTMF = (uuid, key) => {
+ RNCallKeepModule.sendDTMF(uuid, key);
+ }
+
checkIfBusy = () =>
isIOS
? RNCallKeepModule.checkIfBusy()
@@ -108,14 +139,22 @@ class RNCallKeep {
RNCallKeepModule.setAvailable(state);
};
- setCurrentCallActive = () => {
+ setCurrentCallActive = (callUUID) => {
if (isIOS) {
return;
}
- RNCallKeepModule.setCurrentCallActive();
+ RNCallKeepModule.setCurrentCallActive(callUUID);
};
+ updateDisplay = (uuid, displayName, uri) => {
+ RNCallKeepModule.updateDisplay(uuid, displayName, uri)
+ }
+
+ setOnHold = (uuid, shouldHold) => {
+ RNCallKeepModule.setOnHold(uuid, shouldHold);
+ }
+
reportUpdatedCall = (uuid, localizedCallerName) =>
isIOS
? RNCallKeepModule.reportUpdatedCall(uuid, localizedCallerName)
@@ -133,8 +172,10 @@ class RNCallKeep {
});
_setupAndroid = async (options) => {
- const hasAccount = await RNCallKeepModule.checkPhoneAccountPermission();
- const shouldOpenAccounts = await this._alert(options, hasAccount);
+ RNCallKeepModule.setup(options);
+
+ const showAccountAlert = await RNCallKeepModule.checkPhoneAccountPermission(options.additionalPermissions || []);
+ const shouldOpenAccounts = await this._alert(options, showAccountAlert);
if (shouldOpenAccounts) {
RNCallKeepModule.openPhoneAccounts();
@@ -151,7 +192,7 @@ class RNCallKeep {
};
_alert = async (options, condition) => new Promise((resolve, reject) => {
- if (condition) {
+ if (!condition) {
return resolve(false);
}
@@ -180,11 +221,6 @@ class RNCallKeep {
NativeModules.RNCallKeep.backToForeground();
}
- /*
- static holdCall(uuid, onHold) {
- RNCallKeepModule.setHeldCall(uuid, onHold);
- }
- */
}
export default new RNCallKeep();
diff --git a/ios/RNCallKeep/RNCallKeep.m b/ios/RNCallKeep/RNCallKeep.m
index a581728b..4c08461a 100644
--- a/ios/RNCallKeep/RNCallKeep.m
+++ b/ios/RNCallKeep/RNCallKeep.m
@@ -22,10 +22,12 @@
static NSString *const RNCallKeepPerformAnswerCallAction = @"RNCallKeepPerformAnswerCallAction";
static NSString *const RNCallKeepPerformEndCallAction = @"RNCallKeepPerformEndCallAction";
static NSString *const RNCallKeepDidActivateAudioSession = @"RNCallKeepDidActivateAudioSession";
+static NSString *const RNCallKeepDidDeactivateAudioSession = @"RNCallKeepDidDeactivateAudioSession";
static NSString *const RNCallKeepDidDisplayIncomingCall = @"RNCallKeepDidDisplayIncomingCall";
static NSString *const RNCallKeepDidPerformSetMutedCallAction = @"RNCallKeepDidPerformSetMutedCallAction";
static NSString *const RNCallKeepPerformPlayDTMFCallAction = @"RNCallKeepDidPerformDTMFAction";
static NSString *const RNCallKeepDidToggleHoldAction = @"RNCallKeepDidToggleHoldAction";
+static NSString *const RNCallKeepProviderReset = @"RNCallKeepProviderReset";
@implementation RNCallKeep
{
@@ -72,10 +74,12 @@ - (void)dealloc
RNCallKeepPerformAnswerCallAction,
RNCallKeepPerformEndCallAction,
RNCallKeepDidActivateAudioSession,
+ RNCallKeepDidDeactivateAudioSession,
RNCallKeepDidDisplayIncomingCall,
RNCallKeepDidPerformSetMutedCallAction,
RNCallKeepPerformPlayDTMFCallAction,
- RNCallKeepDidToggleHoldAction
+ RNCallKeepDidToggleHoldAction,
+ RNCallKeepProviderReset
];
}
@@ -148,9 +152,9 @@ - (void)dealloc
RCT_EXPORT_METHOD(startCall:(NSString *)uuidString
handle:(NSString *)handle
+ contactIdentifier:(NSString * _Nullable)contactIdentifier
handleType:(NSString *)handleType
- video:(BOOL)video
- contactIdentifier:(NSString * _Nullable)contactIdentifier)
+ video:(BOOL)video)
{
#ifdef DEBUG
NSLog(@"[RNCallKeep][startCall] uuidString = %@", uuidString);
@@ -191,13 +195,13 @@ - (void)dealloc
}
}
-RCT_EXPORT_METHOD(setHeldCall:(NSString *)uuidString onHold:(BOOL)onHold)
+RCT_EXPORT_METHOD(setOnHold:(NSString *)uuidString :(BOOL)shouldHold)
{
#ifdef DEBUG
- NSLog(@"[RNCallKeep][setHeldCall] uuidString = %@", uuidString);
+ NSLog(@"[RNCallKeep][setOnHold] uuidString = %@, shouldHold = %d", uuidString, shouldHold);
#endif
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
- CXSetHeldCallAction *setHeldCallAction = [[CXSetHeldCallAction alloc] initWithCallUUID:uuid onHold:onHold];
+ CXSetHeldCallAction *setHeldCallAction = [[CXSetHeldCallAction alloc] initWithCallUUID:uuid onHold:shouldHold];
CXTransaction *transaction = [[CXTransaction alloc] init];
[transaction addAction:setHeldCallAction];
@@ -209,13 +213,53 @@ - (void)dealloc
_isStartCallActionEventListenerAdded = YES;
}
+RCT_EXPORT_METHOD(reportConnectingOutgoingCallWithUUID:(NSString *)uuidString)
+{
+ NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
+ [self.callKeepProvider reportOutgoingCallWithUUID:uuid startedConnectingAtDate:[NSDate date]];
+}
+
RCT_EXPORT_METHOD(reportConnectedOutgoingCallWithUUID:(NSString *)uuidString)
{
NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
[self.callKeepProvider reportOutgoingCallWithUUID:uuid connectedAtDate:[NSDate date]];
}
-RCT_EXPORT_METHOD(setMutedCall:(NSString *)uuidString muted:(BOOL)muted)
+RCT_EXPORT_METHOD(reportEndCallWithUUID:(NSString *)uuidString :(int)reason)
+{
+#ifdef DEBUG
+ NSLog(@"[RNCallKeep][reportEndCallWithUUID] uuidString = %@ reason = %d", uuidString, reason);
+#endif
+ NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
+ switch (reason) {
+ case CXCallEndedReasonFailed:
+ [self.callKeepProvider reportCallWithUUID:uuid endedAtDate:[NSDate date] reason:CXCallEndedReasonFailed];
+ break;
+ case CXCallEndedReasonRemoteEnded:
+ [self.callKeepProvider reportCallWithUUID:uuid endedAtDate:[NSDate date] reason:CXCallEndedReasonRemoteEnded];
+ break;
+ case CXCallEndedReasonUnanswered:
+ [self.callKeepProvider reportCallWithUUID:uuid endedAtDate:[NSDate date] reason:CXCallEndedReasonUnanswered];
+ break;
+ default:
+ break;
+ }
+}
+
+RCT_EXPORT_METHOD(updateDisplay:(NSString *)uuidString :(NSString *)displayName :(NSString *)uri)
+{
+#ifdef DEBUG
+ NSLog(@"[RNCallKeep][updateDisplay] uuidString = %@ displayName = %@ uri = %@", uuidString, displayName, uri);
+#endif
+ NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
+ CXHandle *callHandle = [[CXHandle alloc] initWithType:CXHandleTypePhoneNumber value:uri];
+ CXCallUpdate *callUpdate = [[CXCallUpdate alloc] init];
+ callUpdate.localizedCallerName = displayName;
+ callUpdate.remoteHandle = callHandle;
+ [self.callKeepProvider reportCallWithUUID:uuid updated:callUpdate];
+}
+
+RCT_EXPORT_METHOD(setMutedCall:(NSString *)uuidString :(BOOL)muted)
{
#ifdef DEBUG
NSLog(@"[RNCallKeep][setMutedCall] muted = %i", muted);
@@ -228,6 +272,19 @@ - (void)dealloc
[self requestTransaction:transaction];
}
+RCT_EXPORT_METHOD(sendDTMF:(NSString *)uuidString dtmf:(NSString *)key)
+{
+#ifdef DEBUG
+ NSLog(@"[RNCallKeep][sendDTMF] key = %@", key);
+#endif
+ NSUUID *uuid = [[NSUUID alloc] initWithUUIDString:uuidString];
+ CXPlayDTMFCallAction *dtmfAction = [[CXPlayDTMFCallAction alloc] initWithCallUUID:uuid digits:key type:CXPlayDTMFCallActionTypeHardPause];
+ CXTransaction *transaction = [[CXTransaction alloc] init];
+ [transaction addAction:dtmfAction];
+
+ [self requestTransaction:transaction];
+}
+
- (void)requestTransaction:(CXTransaction *)transaction
{
#ifdef DEBUG
@@ -270,12 +327,6 @@ - (BOOL)lessThanIos10_2
}
}
-- (BOOL)containsLowerCaseLetter:(NSString *)callUUID
-{
- NSRegularExpression* regex = [[NSRegularExpression alloc] initWithPattern:@"[a-z]" options:0 error:nil];
- return [regex numberOfMatchesInString:callUUID options:0 range:NSMakeRange(0, [callUUID length])] > 0;
-}
-
- (int)getHandleType:(NSString *)handleType
{
int _handleType;
@@ -300,7 +351,16 @@ - (CXProviderConfiguration *)getProviderConfiguration
providerConfiguration.supportsVideo = YES;
providerConfiguration.maximumCallGroups = 3;
providerConfiguration.maximumCallsPerCallGroup = 1;
- providerConfiguration.supportedHandleTypes = [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypePhoneNumber], [NSNumber numberWithInteger:CXHandleTypeEmailAddress], [NSNumber numberWithInteger:CXHandleTypeGeneric], nil];
+ providerConfiguration.supportedHandleTypes = [NSSet setWithObjects:[NSNumber numberWithInteger:CXHandleTypePhoneNumber], nil];
+ if (_settings[@"supportsVideo"]) {
+ providerConfiguration.supportsVideo = _settings[@"supportsVideo"];
+ }
+ if (_settings[@"maximumCallGroups"]) {
+ providerConfiguration.maximumCallGroups = [_settings[@"maximumCallGroups"] integerValue];
+ }
+ if (_settings[@"maximumCallsPerCallGroup"]) {
+ providerConfiguration.maximumCallsPerCallGroup = [_settings[@"maximumCallsPerCallGroup"] integerValue];
+ }
if (_settings[@"imageName"]) {
providerConfiguration.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:_settings[@"imageName"]]);
}
@@ -317,7 +377,7 @@ - (void)configureAudioSession
#endif
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
- [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
+ [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionAllowBluetooth error:nil];
[audioSession setMode:AVAudioSessionModeVoiceChat error:nil];
@@ -421,6 +481,9 @@ - (void)providerDidReset:(CXProvider *)provider{
#ifdef DEBUG
NSLog(@"[RNCallKeep][providerDidReset]");
#endif
+ //this means something big changed, so tell the JS. The JS should
+ //probably respond by hanging up all calls.
+ [self sendEventWithName:RNCallKeepProviderReset body:nil];
}
// Starting outgoing call
@@ -429,8 +492,10 @@ - (void)provider:(CXProvider *)provider performStartCallAction:(CXStartCallActio
#ifdef DEBUG
NSLog(@"[RNCallKeep][CXProviderDelegate][provider:performStartCallAction]");
#endif
- [self.callKeepProvider reportOutgoingCallWithUUID:action.callUUID startedConnectingAtDate:[NSDate date]];
+ //do this first, audio sessions are flakey
[self configureAudioSession];
+ //tell the JS to actually make the call
+ [self sendEventWithName:RNCallKeepDidReceiveStartCallAction body:@{ @"callUUID": [action.callUUID.UUIDString lowercaseString], @"handle": action.handle.value }];
[action fulfill];
}
@@ -453,11 +518,8 @@ - (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAct
#ifdef DEBUG
NSLog(@"[RNCallKeep][CXProviderDelegate][provider:performAnswerCallAction]");
#endif
- if (![self lessThanIos10_2]) {
- [self configureAudioSession];
- }
- NSString *callUUID = [self containsLowerCaseLetter:action.callUUID.UUIDString] ? action.callUUID.UUIDString : [action.callUUID.UUIDString lowercaseString];
- [self sendEventWithName:RNCallKeepPerformAnswerCallAction body:@{ @"callUUID": callUUID }];
+ [self configureAudioSession];
+ [self sendEventWithName:RNCallKeepPerformAnswerCallAction body:@{ @"callUUID": [action.callUUID.UUIDString lowercaseString] }];
[action fulfill];
}
@@ -467,8 +529,7 @@ - (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)
#ifdef DEBUG
NSLog(@"[RNCallKeep][CXProviderDelegate][provider:performEndCallAction]");
#endif
- NSString *callUUID = [self containsLowerCaseLetter:action.callUUID.UUIDString] ? action.callUUID.UUIDString : [action.callUUID.UUIDString lowercaseString];
- [self sendEventWithName:RNCallKeepPerformEndCallAction body:@{ @"callUUID": callUUID }];
+ [self sendEventWithName:RNCallKeepPerformEndCallAction body:@{ @"callUUID": [action.callUUID.UUIDString lowercaseString] }];
[action fulfill];
}
@@ -477,9 +538,8 @@ -(void)provider:(CXProvider *)provider performSetHeldCallAction:(CXSetHeldCallAc
#ifdef DEBUG
NSLog(@"[RNCallKeep][CXProviderDelegate][provider:performSetHeldCallAction]");
#endif
- NSString *callUUID = [self containsLowerCaseLetter:action.callUUID.UUIDString] ? action.callUUID.UUIDString : [action.callUUID.UUIDString lowercaseString];
- [self sendEventWithName:RNCallKeepDidToggleHoldAction body:@{ @"hold": @(action.onHold), @"callUUID": callUUID }];
+ [self sendEventWithName:RNCallKeepDidToggleHoldAction body:@{ @"hold": @(action.onHold), @"callUUID": [action.callUUID.UUIDString lowercaseString] }];
[action fulfill];
}
@@ -487,8 +547,17 @@ - (void)provider:(CXProvider *)provider performPlayDTMFCallAction:(CXPlayDTMFCal
#ifdef DEBUG
NSLog(@"[RNCallKeep][CXProviderDelegate][provider:performPlayDTMFCallAction]");
#endif
- NSString *callUUID = [self containsLowerCaseLetter:action.callUUID.UUIDString] ? action.callUUID.UUIDString : [action.callUUID.UUIDString lowercaseString];
- [self sendEventWithName:RNCallKeepPerformPlayDTMFCallAction body:@{ @"digits": action.digits, @"callUUID": callUUID }];
+ [self sendEventWithName:RNCallKeepPerformPlayDTMFCallAction body:@{ @"digits": action.digits, @"callUUID": [action.callUUID.UUIDString lowercaseString] }];
+ [action fulfill];
+}
+
+-(void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action
+{
+#ifdef DEBUG
+ NSLog(@"[RNCallKeep][CXProviderDelegate][provider:performSetMutedCallAction]");
+#endif
+
+ [self sendEventWithName:RNCallKeepDidPerformSetMutedCallAction body:@{ @"muted": @(action.muted), @"callUUID": [action.callUUID.UUIDString lowercaseString] }];
[action fulfill];
}
@@ -504,6 +573,7 @@ - (void)provider:(CXProvider *)provider didActivateAudioSession:(AVAudioSession
#ifdef DEBUG
NSLog(@"[RNCallKeep][CXProviderDelegate][provider:didActivateAudioSession]");
#endif
+ [self configureAudioSession];
[self sendEventWithName:RNCallKeepDidActivateAudioSession body:nil];
}
@@ -512,15 +582,7 @@ - (void)provider:(CXProvider *)provider didDeactivateAudioSession:(AVAudioSessio
#ifdef DEBUG
NSLog(@"[RNCallKeep][CXProviderDelegate][provider:didDeactivateAudioSession]");
#endif
-}
-
--(void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action
-{
-#ifdef DEBUG
- NSLog(@"[RNCallKeep][CXProviderDelegate][provider:performSetMutedCallAction]");
-#endif
- [self sendEventWithName:RNCallKeepDidPerformSetMutedCallAction body:@{ @"muted": @(action.muted) }];
- [action fulfill];
+ [self sendEventWithName:RNCallKeepDidDeactivateAudioSession body:nil];
}
@end
diff --git a/package.json b/package.json
index 61d5bed5..18e44c94 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-native-callkeep",
- "version": "2.0.3",
+ "version": "3.0.0",
"description": "iOS 10 CallKit and Android ConnectionService Framework For React Native",
"main": "index.js",
"scripts": {},