diff --git a/CHANGELOG.md b/CHANGELOG.md index 3004414e..1c8468b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,14 +3,20 @@ ## 4.0.0 - Android + - implement new autolinking react native API - update Firebase Messaging to 17.3.4 which simplifies how to obtain the FCM token - Android X migration - use gradle 5.4.1 - use API 28 - - upgrade com.twilio:voice-android to 2.1.0 + - upgrade com.twilio:voice-android to 3.3.0 + - implement hold call Twilio API + - implement call ringing Twilio event + - remove `call_state` from CallInvite - iOS - - convert params for connectionDidConnect to => call_to, from => call_from - - convert params for connectionDidDisconnect to => call_to, from => call_from, error => err + - convert params for `connectionDidConnect` to => `call_to`, from => `call_from` + - convert params for `connectionDidDisconnect` to => `call_to`, from => `call_from`, `error` => `err` + +- throw an error when listening to events that do not exist ## 3.21.3 diff --git a/README.md b/README.md index 56604f77..a2623f41 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ This is a React Native wrapper for Twilio Programmable Voice SDK that lets you m # Twilio Programmable Voice SDK -- Android 2.1.0 (bundled within this library) +- Android 3.3.0 (bundled within this library) - iOS 2.1.0 (specified by the app's own podfile) ## Breaking changes in v4.0.0 @@ -21,6 +21,8 @@ This is a React Native wrapper for Twilio Programmable Voice SDK that lets you m ``` +Data passed to the event `deviceDidReceiveIncoming` does not contain the key `call_state`, because state of Call Invites was removed in Twilio Android v3.0.0 + - iOS: params changes for `connectionDidConnect` and `connectionDidDisconnect` to => call_to @@ -254,14 +256,14 @@ TwilioVoice.addEventListener('connectionDidConnect', function(data) { // Android // { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', + // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // } // iOS // { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', + // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'DISCONNECTED' | 'CANCELLED', // from: string, // "+441234567890" // issue 44 (https://github.com/hoxfon/react-native-twilio-programmable-voice/issues/44) // to: string, // "client:bob" // issue 44 (https://github.com/hoxfon/react-native-twilio-programmable-voice/issues/44) // } @@ -274,7 +276,7 @@ TwilioVoice.addEventListener('connectionDidDisconnect', function(data: mixed) { // | Android // { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', + // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // err?: string, @@ -282,7 +284,7 @@ TwilioVoice.addEventListener('connectionDidDisconnect', function(data: mixed) { // | iOS // { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', + // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' |'DISCONNECTED' | 'CANCELLED', // call_from?: string, // "+441234567890" // call_to?: string, // "client:bob" // from?: string, // "+441234567890" // issue 44 (https://github.com/hoxfon/react-native-twilio-programmable-voice/issues/44) @@ -298,7 +300,7 @@ TwilioVoice.addEventListener('callRejected', function(value: 'callRejected') {}) TwilioVoice.addEventListener('deviceDidReceiveIncoming', function(data) { // { // call_sid: string, // Twilio call sid - // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' 'DISCONNECTED' | 'CANCELLED', + // call_state: 'PENDING' | 'CONNECTED' | 'ACCEPTED' | 'CONNECTING' | 'RINGING' | 'DISCONNECTED' | 'CANCELLED', ==> this is removed in v4 // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // } @@ -339,6 +341,10 @@ TwilioVoice.ignore() // mutedValue must be a boolean TwilioVoice.setMuted(mutedValue) +// put a call on hold +TwilioVoice.setOnHold(holdValue) + +// send digits TwilioVoice.sendDigits(digits) // should be called after the app is initialized diff --git a/android/build.gradle b/android/build.gradle index b4365da1..fbe2ff3a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,6 +4,7 @@ buildscript { repositories { google() jcenter() + google() } dependencies { classpath 'com.android.tools.build:gradle:3.5.2' @@ -26,7 +27,7 @@ apply plugin: 'com.android.library' def DEFAULT_COMPILE_SDK_VERSION = 28 def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3" def DEFAULT_TARGET_SDK_VERSION = 28 -def DEFAULT_SUPPORT_LIB_VERSION = "28.0.0" +def DEFAULT_SUPPORT_LIB_VERSION = "28.0.3" android { compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION @@ -50,7 +51,7 @@ dependencies { def supportLibVersion = rootProject.hasProperty('supportLibVersion') ? rootProject.supportLibVersion : DEFAULT_SUPPORT_LIB_VERSION implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.twilio:voice-android:2.1.0' + implementation 'com.twilio:voice-android:3.3.0' implementation "com.android.support:appcompat-v7:$supportLibVersion" implementation 'com.facebook.react:react-native:+' implementation 'com.google.firebase:firebase-messaging:17.+' diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java index 6bf9bcdc..af4e9175 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java @@ -21,6 +21,7 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.twilio.voice.CallInvite; +import com.twilio.voice.CancelledCallInvite; import java.util.List; @@ -307,12 +308,18 @@ public void createHangupLocalNotification(ReactApplicationContext context, Strin } public void removeIncomingCallNotification(ReactApplicationContext context, - CallInvite callInvite, + CancelledCallInvite callInvite, int notificationId) { - Log.d(TAG, "removeIncomingCallNotification"); + if (BuildConfig.DEBUG) { + Log.d(TAG, "removeIncomingCallNotification"); + } + if (context == null) { + Log.e(TAG, "Context is null"); + return; + } NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { - if (callInvite != null && callInvite.getState() == CallInvite.State.PENDING) { + if (callInvite != null) { /* * If the incoming call message was cancelled then remove the notification by matching * it with the call sid from the list of notifications in the notification drawer. diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java index af7a98aa..00359270 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java @@ -21,6 +21,9 @@ public class EventManager { public static final String EVENT_CONNECTION_DID_CONNECT = "connectionDidConnect"; public static final String EVENT_CONNECTION_DID_DISCONNECT = "connectionDidDisconnect"; public static final String EVENT_DEVICE_DID_RECEIVE_INCOMING = "deviceDidReceiveIncoming"; + public static final String EVENT_CALL_STATE_RINGING = "callStateRinging"; + + public EventManager(ReactApplicationContext context) { mContext = context; diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java index 64a2b54e..f1171802 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java @@ -43,15 +43,20 @@ import com.google.android.gms.tasks.Task; import com.google.firebase.iid.FirebaseInstanceId; import com.google.firebase.iid.InstanceIdResult; +import com.twilio.voice.AcceptOptions; import com.twilio.voice.Call; import com.twilio.voice.CallException; import com.twilio.voice.CallInvite; +import com.twilio.voice.ConnectOptions; import com.twilio.voice.LogLevel; import com.twilio.voice.RegistrationException; import com.twilio.voice.RegistrationListener; +import com.twilio.voice.StatsListener; +import com.twilio.voice.StatsReport; import com.twilio.voice.Voice; import java.util.HashMap; +import java.util.List; import java.util.Map; import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CONNECTION_DID_CONNECT; @@ -59,6 +64,7 @@ import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_DEVICE_DID_RECEIVE_INCOMING; import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_DEVICE_NOT_READY; import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_DEVICE_READY; +import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CALL_STATE_RINGING; public class TwilioVoiceModule extends ReactContextBaseJavaModule implements ActivityEventListener, LifecycleEventListener { @@ -78,6 +84,7 @@ public class TwilioVoiceModule extends ReactContextBaseJavaModule implements Act public static final String INCOMING_CALL_INVITE = "INCOMING_CALL_INVITE"; public static final String INCOMING_CALL_NOTIFICATION_ID = "INCOMING_CALL_NOTIFICATION_ID"; public static final String NOTIFICATION_TYPE = "NOTIFICATION_TYPE"; + public static final String CANCELLED_CALL_INVITE = "CANCELLED_CALL_INVITE"; public static final String ACTION_INCOMING_CALL = "com.hoxfon.react.TwilioVoice.INCOMING_CALL"; public static final String ACTION_FCM_TOKEN = "com.hoxfon.react.TwilioVoice.ACTION_FCM_TOKEN"; @@ -85,6 +92,7 @@ public class TwilioVoiceModule extends ReactContextBaseJavaModule implements Act public static final String ACTION_ANSWER_CALL = "com.hoxfon.react.TwilioVoice.ANSWER_CALL"; public static final String ACTION_REJECT_CALL = "com.hoxfon.react.TwilioVoice.REJECT_CALL"; public static final String ACTION_HANGUP_CALL = "com.hoxfon.react.TwilioVoice.HANGUP_CALL"; + public static final String ACTION_CANCEL_CALL_INVITE = "com.hoxfon.react.TwilioVoice.CANCEL_CALL_INVITE"; public static final String ACTION_CLEAR_MISSED_CALLS_COUNT = "com.hoxfon.react.TwilioVoice.CLEAR_MISSED_CALLS_COUNT"; public static final String CALL_SID_KEY = "CALL_SID"; @@ -217,6 +225,33 @@ public void onError(RegistrationException error, String accessToken, String fcmT private Call.Listener callListener() { return new Call.Listener() { + /* + * This callback is emitted once before the Call.Listener.onConnected() callback when + * the callee is being alerted of a Call. The behavior of this callback is determined by + * the answerOnBridge flag provided in the Dial verb of your TwiML application + * associated with this client. If the answerOnBridge flag is false, which is the + * default, the Call.Listener.onConnected() callback will be emitted immediately after + * Call.Listener.onRinging(). If the answerOnBridge flag is true, this will cause the + * call to emit the onConnected callback only after the call is answered. + * See answeronbridge for more details on how to use it with the Dial TwiML verb. If the + * twiML response contains a Say verb, then the call will emit the + * Call.Listener.onConnected callback immediately after Call.Listener.onRinging() is + * raised, irrespective of the value of answerOnBridge being set to true or false + */ + @Override + public void onRinging(Call call) { + // TODO test this with JS app + if (BuildConfig.DEBUG) { + Log.d(TAG, "CALL RINGING callListener().onRinging call state = "+call.getState()); + Log.d(TAG, call.toString()); + } + WritableMap params = Arguments.createMap(); + if (call != null) { + params.putString("call_sid", call.getSid()); + params.putString("call_state", call.getState().name()); + } + eventManager.sendEvent(EVENT_CALL_STATE_RINGING, params); + } @Override public void onConnected(Call call) { if (BuildConfig.DEBUG) { @@ -245,6 +280,27 @@ public void onConnected(Call call) { eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params); } +// Prepare methods Twilio for v4 +// @Override +// public void onReconnecting(Call call, CallException callException) { +// if (BuildConfig.DEBUG) { +// Log.d(TAG, "CALL RECONNECTING callListener().onReconnecting call state = "+call.getState()); +// } +// // TODO implement +//// eventManager.sendEvent(XXX, params); +// +// } +// +// @Override +// public void onReconnected(Call call) { +// if (BuildConfig.DEBUG) { +// Log.d(TAG, "CALL RECONNECTED callListener().onReconnected call state = "+call.getState()); +// } +// +// // TODO implement +//// eventManager.sendEvent(XXX, params); +// } + @Override public void onDisconnected(Call call, CallException error) { unsetAudioFocus(); @@ -277,6 +333,16 @@ public void onDisconnected(Call call, CallException error) { callNotificationManager.removeHangupNotification(getReactApplicationContext()); toNumber = ""; toName = ""; + + if (BuildConfig.DEBUG) { + call.getStats(new StatsListener() { + @Override + public void onStats(@NonNull List statsReports) { + // Process statsReports + Log.d(TAG, "Call stats: "+statsReports.toString()); + } + }); + } } @Override @@ -319,6 +385,7 @@ private void registerReceiver() { if (!isReceiverRegistered) { IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ACTION_INCOMING_CALL); + intentFilter.addAction(ACTION_CANCEL_CALL_INVITE); intentFilter.addAction(ACTION_MISSED_CALL); LocalBroadcastManager.getInstance(getReactApplicationContext()).registerReceiver( voiceBroadcastReceiver, intentFilter); @@ -381,19 +448,13 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } private void handleIncomingCallIntent(Intent intent) { - if (intent == null || intent.getAction() == null) { - Log.e(TAG, "handleIncomingCallIntent intent is null"); - return; - } - if (intent.getAction().equals(ACTION_INCOMING_CALL)) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "handleIncomingCallIntent"); + } activeCallInvite = intent.getParcelableExtra(INCOMING_CALL_INVITE); - - if (activeCallInvite != null && (activeCallInvite.getState() == CallInvite.State.PENDING)) { + if (activeCallInvite != null) { callAccepted = false; - if (BuildConfig.DEBUG) { - Log.d(TAG, "handleIncomingCallIntent state = PENDING"); - } SoundPoolManager.getInstance(getReactApplicationContext()).playRinging(); if (getReactApplicationContext().getCurrentActivity() != null) { @@ -411,58 +472,35 @@ private void handleIncomingCallIntent(Intent intent) { WritableMap params = Arguments.createMap(); params.putString("call_sid", activeCallInvite.getCallSid()); params.putString("call_from", activeCallInvite.getFrom()); - params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); + params.putString("call_to", activeCallInvite.getTo()); // this is not needed eventManager.sendEvent(EVENT_DEVICE_DID_RECEIVE_INCOMING, params); } - - } else { + // TODO evaluate what more is needed at this point? + Log.e(TAG, "ACTION_INCOMING_CALL but not active call"); + } + } else if (intent.getAction().equals(ACTION_CANCEL_CALL_INVITE)) { + SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "activeCallInvite was cancelled by " + activeCallInvite.getFrom()); + } + if (!callAccepted) { if (BuildConfig.DEBUG) { - Log.d(TAG, "====> BEGIN handleIncomingCallIntent when activeCallInvite != PENDING"); - } - // this block is executed when the callInvite is cancelled and: - // - the call is answered (activeCall != null) - // - the call is rejected - - SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); - - // the call is not active yet - if (activeCall == null) { - - if (activeCallInvite != null) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "activeCallInvite state = " + activeCallInvite.getState()); - } - if (BuildConfig.DEBUG) { - Log.d(TAG, "activeCallInvite was cancelled by " + activeCallInvite.getFrom()); - } - if (!callAccepted) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "creating a missed call, activeCallInvite state: " + activeCallInvite.getState()); - } - callNotificationManager.createMissedCallNotification(getReactApplicationContext(), activeCallInvite); - int appImportance = callNotificationManager.getApplicationImportance(getReactApplicationContext()); - if (appImportance != RunningAppProcessInfo.IMPORTANCE_BACKGROUND) { - WritableMap params = Arguments.createMap(); - params.putString("call_sid", activeCallInvite.getCallSid()); - params.putString("call_from", activeCallInvite.getFrom()); - params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); - eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); - } - } - } - clearIncomingNotification(activeCallInvite); - } else { - if (BuildConfig.DEBUG) { - Log.d(TAG, "activeCallInvite was answered. Call " + activeCall); - } + Log.d(TAG, "creating a missed call"); } - if (BuildConfig.DEBUG) { - Log.d(TAG, "====> END"); + callNotificationManager.createMissedCallNotification(getReactApplicationContext(), activeCallInvite); + int appImportance = callNotificationManager.getApplicationImportance(getReactApplicationContext()); + if (appImportance != RunningAppProcessInfo.IMPORTANCE_BACKGROUND) { + WritableMap params = Arguments.createMap(); + params.putString("call_sid", activeCallInvite.getCallSid()); + params.putString("call_from", activeCallInvite.getFrom()); + params.putString("call_to", activeCallInvite.getTo()); + // TODO fix constant + params.putString("call_state", "DISCONNECTED"); + eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); } } + clearIncomingNotification(activeCallInvite); } else if (intent.getAction().equals(ACTION_FCM_TOKEN)) { if (BuildConfig.DEBUG) { Log.d(TAG, "handleIncomingCallIntent ACTION_FCM_TOKEN"); @@ -476,11 +514,14 @@ private class VoiceBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "VoiceBroadcastReceiver.onReceive "+action+". Intent "+ intent.getExtras()); + } if (action.equals(ACTION_INCOMING_CALL)) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "VoiceBroadcastReceiver.onReceive ACTION_INCOMING_CALL. Intent "+ intent.getExtras()); - } handleIncomingCallIntent(intent); + } else if (action.equals(ACTION_CANCEL_CALL_INVITE)) { + // TODO check if implementation need something else + clearIncomingNotification(activeCallInvite); } else if (action.equals(ACTION_MISSED_CALL)) { SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences(PREFERENCE_KEY, Context.MODE_PRIVATE); SharedPreferences.Editor sharedPrefEditor = sharedPref.edit(); @@ -516,7 +557,7 @@ public void initWithAccessToken(final String accessToken, Promise promise) { private void clearIncomingNotification(CallInvite callInvite) { if (BuildConfig.DEBUG) { - Log.d(TAG, "clearIncomingNotification() callInvite state: "+ callInvite.getState()); + Log.d(TAG, "clearIncomingNotification() callInvite: "+ callInvite.toString()); } if (callInvite != null && callInvite.getCallSid() != null) { // remove incoming call notification @@ -528,7 +569,7 @@ private void clearIncomingNotification(CallInvite callInvite) { callNotificationManager.removeIncomingCallNotification(getReactApplicationContext(), null, notificationId); TwilioVoiceModule.callNotificationMap.remove(notificationKey); } -// activeCallInvite = null; + activeCallInvite = null; } /* @@ -554,11 +595,10 @@ public void onComplete(@NonNull Task task) { if (BuildConfig.DEBUG) { Log.d(TAG, "Registering with FCM"); } - Voice.register(getReactApplicationContext(), accessToken, Voice.RegistrationChannel.FCM, fcmToken, registrationListener); + Voice.register(accessToken, Voice.RegistrationChannel.FCM, fcmToken, registrationListener); } } }); - } @ReactMethod @@ -566,26 +606,28 @@ public void accept() { callAccepted = true; SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); if (activeCallInvite != null){ - if (activeCallInvite.getState() == CallInvite.State.PENDING) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "accept() activeCallInvite.getState() PENDING"); - } - activeCallInvite.accept(getReactApplicationContext(), callListener); - clearIncomingNotification(activeCallInvite); - } else { - // when the user answers a call from a notification before the react-native App - // is completely initialised, and the first event has been skipped - // re-send connectionDidConnect message to JS - WritableMap params = Arguments.createMap(); - params.putString("call_sid", activeCallInvite.getCallSid()); - params.putString("call_from", activeCallInvite.getFrom()); - params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); - callNotificationManager.createHangupLocalNotification(getReactApplicationContext(), - activeCallInvite.getCallSid(), - activeCallInvite.getFrom()); - eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params); + if (BuildConfig.DEBUG) { + Log.d(TAG, "accept()"); } + AcceptOptions acceptOptions = new AcceptOptions.Builder().build(); + activeCallInvite.accept(getReactApplicationContext(), acceptOptions, callListener); + clearIncomingNotification(activeCallInvite); + + // TODO check whether this block is needed +// // when the user answers a call from a notification before the react-native App +// // is completely initialised, and the first event has been skipped +// // re-send connectionDidConnect message to JS +// WritableMap params = Arguments.createMap(); +// params.putString("call_sid", activeCallInvite.getCallSid()); +// params.putString("call_from", activeCallInvite.getFrom()); +// params.putString("call_to", activeCallInvite.getTo()); +// // TODO fix this +// // CallInvite.getState() has been removed in Twilio v3.0 +// // params.putString("call_state", activeCallInvite.getState().name()); +// callNotificationManager.createHangupLocalNotification(getReactApplicationContext(), +// activeCallInvite.getCallSid(), +// activeCallInvite.getFrom()); +// eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params); } else { eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, null); } @@ -596,11 +638,12 @@ public void reject() { callAccepted = false; SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); WritableMap params = Arguments.createMap(); - if (activeCallInvite != null){ - params.putString("call_sid", activeCallInvite.getCallSid()); - params.putString("call_from", activeCallInvite.getFrom()); - params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); + if (activeCallInvite != null) { + params.putString("call_sid", activeCallInvite.getCallSid()); + params.putString("call_from", activeCallInvite.getFrom()); + params.putString("call_to", activeCallInvite.getTo()); + // TODO fix constant + params.putString("call_state", "CONNECTED"); activeCallInvite.reject(getReactApplicationContext()); clearIncomingNotification(activeCallInvite); } @@ -612,11 +655,12 @@ public void ignore() { callAccepted = false; SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); WritableMap params = Arguments.createMap(); - if (activeCallInvite != null){ - params.putString("call_sid", activeCallInvite.getCallSid()); - params.putString("call_from", activeCallInvite.getFrom()); - params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); + if (activeCallInvite != null) { + params.putString("call_sid", activeCallInvite.getCallSid()); + params.putString("call_from", activeCallInvite.getFrom()); + params.putString("call_to", activeCallInvite.getTo()); + // TODO fix constant + params.putString("call_state", "CONNECTED"); clearIncomingNotification(activeCallInvite); } eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); @@ -673,7 +717,11 @@ public void connect(ReadableMap params) { } } - activeCall = Voice.call(getReactApplicationContext(), accessToken, twiMLParams, callListener); + ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken) + .params(twiMLParams) + .build(); + + activeCall = Voice.connect(getReactApplicationContext(), connectOptions, callListener); } @ReactMethod @@ -713,14 +761,13 @@ public void getActiveCall(Promise promise) { return; } if (activeCallInvite != null) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "Active call invite found state = "+activeCallInvite.getState()); - } +// if (BuildConfig.DEBUG) { +// Log.d(TAG, "Active call invite found state = "+activeCallInvite.getState()); +// } WritableMap params = Arguments.createMap(); params.putString("call_sid", activeCallInvite.getCallSid()); params.putString("call_from", activeCallInvite.getFrom()); params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); promise.resolve(params); return; } @@ -734,6 +781,13 @@ public void setSpeakerPhone(Boolean value) { audioManager.setSpeakerphoneOn(value); } + @ReactMethod + public void setOnHold(Boolean value) { + if (activeCall != null) { + activeCall.hold(value); + } + } + private void setAudioFocus() { if (audioManager == null) { return; @@ -755,11 +809,12 @@ public void onAudioFocusChange(int i) { } .build(); audioManager.requestAudioFocus(focusRequest); } else { - audioManager.requestAudioFocus( - null, - AudioManager.STREAM_VOICE_CALL, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE - ); + int focusRequestResult = audioManager.requestAudioFocus(new AudioManager.OnAudioFocusChangeListener() { + @Override + public void onAudioFocusChange(int focusChange) {} + }, + AudioManager.STREAM_VOICE_CALL, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); } /* * Start by setting MODE_IN_COMMUNICATION as default audio mode. It is diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java index ca5d4fed..1619d5e4 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java @@ -1,7 +1,5 @@ package com.hoxfon.react.RNTwilioVoice.fcm; -import android.annotation.TargetApi; - import android.app.ActivityManager; import android.content.Intent; import android.os.Handler; @@ -19,7 +17,7 @@ import com.hoxfon.react.RNTwilioVoice.BuildConfig; import com.hoxfon.react.RNTwilioVoice.CallNotificationManager; import com.twilio.voice.CallInvite; -import com.twilio.voice.MessageException; +import com.twilio.voice.CancelledCallInvite; import com.twilio.voice.MessageListener; import com.twilio.voice.Voice; @@ -29,7 +27,9 @@ import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_FCM_TOKEN; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_INCOMING_CALL; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_CANCEL_CALL_INVITE; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_INVITE; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.CANCELLED_CALL_INVITE; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_NOTIFICATION_ID; import com.hoxfon.react.RNTwilioVoice.SoundPoolManager; @@ -71,8 +71,7 @@ public void onMessageReceived(RemoteMessage remoteMessage) { Random randomNumberGenerator = new Random(System.currentTimeMillis()); final int notificationId = randomNumberGenerator.nextInt(); - Voice.handleMessage(this, data, new MessageListener() { - + boolean valid = Voice.handleMessage(data, new MessageListener() { @Override public void onCallInvite(final CallInvite callInvite) { @@ -102,7 +101,10 @@ public void run() { if (appImportance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { context.startActivity(launchIntent); } - VoiceFirebaseMessagingService.this.handleIncomingCall((ReactApplicationContext)context, notificationId, callInvite, launchIntent); + Intent intent = new Intent(ACTION_INCOMING_CALL); + intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); + intent.putExtra(INCOMING_CALL_INVITE, callInvite); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); } else { // Otherwise wait for construction, then handle the incoming call mReactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() { @@ -113,7 +115,13 @@ public void onReactContextInitialized(ReactContext context) { } Intent launchIntent = callNotificationManager.getLaunchIntent((ReactApplicationContext)context, notificationId, callInvite, true, appImportance); context.startActivity(launchIntent); - VoiceFirebaseMessagingService.this.handleIncomingCall((ReactApplicationContext)context, notificationId, callInvite, launchIntent); + Intent intent = new Intent(ACTION_INCOMING_CALL); + intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); + intent.putExtra(INCOMING_CALL_INVITE, callInvite); + LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + callNotificationManager.createIncomingCallNotification( + (ReactApplicationContext) context, callInvite, notificationId, + launchIntent); } }); if (!mReactInstanceManager.hasStartedCreatingInitialContext()) { @@ -126,10 +134,22 @@ public void onReactContextInitialized(ReactContext context) { } @Override - public void onError(MessageException messageException) { - Log.e(TAG, "Error handling FCM message" + messageException.toString()); + public void onCancelledCallInvite(final CancelledCallInvite cancelledCallInvite) { + Handler handler = new Handler(Looper.getMainLooper()); + handler.post(new Runnable() { + public void run() { + ReactInstanceManager mReactInstanceManager = ((ReactApplication) getApplication()).getReactNativeHost().getReactInstanceManager(); + ReactContext context = mReactInstanceManager.getCurrentReactContext(); + VoiceFirebaseMessagingService.this.cancelNotification((ReactApplicationContext)context, cancelledCallInvite); + VoiceFirebaseMessagingService.this.sendCancelledCallInviteToActivity(cancelledCallInvite); + } + }); } }); + + if (!valid) { + Log.e(TAG, "The message was not a valid Twilio Voice SDK payload: " + remoteMessage.getData()); + } } // Check if message contains a notification payload. @@ -138,43 +158,18 @@ public void onError(MessageException messageException) { } } - private void handleIncomingCall(ReactApplicationContext context, - int notificationId, - CallInvite callInvite, - Intent launchIntent - ) { - sendIncomingCallMessageToActivity(context, callInvite, notificationId); - showNotification(context, callInvite, notificationId, launchIntent); - } - /* - * Send the IncomingCallMessage to the TwilioVoiceModule + * Send the CancelledCallInvite to the TwilioVoiceModule */ - private void sendIncomingCallMessageToActivity( - ReactApplicationContext context, - CallInvite callInvite, - int notificationId - ) { - Intent intent = new Intent(ACTION_INCOMING_CALL); - intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); - intent.putExtra(INCOMING_CALL_INVITE, callInvite); - LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + private void sendCancelledCallInviteToActivity(CancelledCallInvite cancelledCallInvite) { + Intent intent = new Intent(ACTION_CANCEL_CALL_INVITE); + intent.putExtra(CANCELLED_CALL_INVITE, cancelledCallInvite); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); } - /* - * Show the notification in the Android notification drawer - */ - @TargetApi(20) - private void showNotification(ReactApplicationContext context, - CallInvite callInvite, - int notificationId, - Intent launchIntent - ) { - if (callInvite != null && callInvite.getState() == CallInvite.State.PENDING) { - callNotificationManager.createIncomingCallNotification(context, callInvite, notificationId, launchIntent); - } else { - SoundPoolManager.getInstance(context.getBaseContext()).stopRinging(); - callNotificationManager.removeIncomingCallNotification(context, callInvite, 0); - } + private void cancelNotification(ReactApplicationContext context, CancelledCallInvite cancelledCallInvite) { + SoundPoolManager.getInstance((this)).stopRinging(); + callNotificationManager.removeIncomingCallNotification(context, cancelledCallInvite, 0); } + } diff --git a/index.js b/index.js index 79dd8841..ba1d9af3 100644 --- a/index.js +++ b/index.js @@ -17,6 +17,7 @@ const _eventHandlers = { deviceDidReceiveIncoming: new Map(), connectionDidConnect: new Map(), connectionDidDisconnect: new Map(), + callStateRinging: new Map(), //iOS specific callRejected: new Map(), } @@ -101,6 +102,10 @@ const Twilio = { } }, addEventListener(type, handler) { + if (!_eventHandlers.hasOwnProperty(type)) { + throw new Error('Event handler not found:', type) + } + if (_eventHandlers[type]) if (_eventHandlers[type].has(handler)) { return }