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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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": {},