diff --git a/CHANGELOG.md b/CHANGELOG.md index db2b8b76..0c925fa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ [Release Section](https://github.com/hoxfon/react-native-twilio-programmable-voice/releases) +## 4.0.0 +- Android + - update Firebase Messaging to 17.3.4 which simplifies how to obtain the FCM token +- iOS + - convert params for connectionDidConnect to => call_to, from => call_from + - convert params for connectionDidDisconnect to => call_to, from => call_from, error => err + ## 3.21.1 - Android: fix crash when asking for microphone permission before an activity is displayed diff --git a/README.md b/README.md index b0e65d07..e7454c2f 100644 --- a/README.md +++ b/README.md @@ -3,41 +3,15 @@ This is a React Native wrapper for Twilio Programmable Voice SDK that lets you m # Twilio Programmable Voice SDK -- Android 2.0.7 (bundled within this library) -- iOS 2.0.4 (specified by the app's own podfile) +- Android 2.0.9 (bundled within this library) +- iOS 2.0.7 (specified by the app's own podfile) -## Breaking changes in v3.0.0 - -- initWitToken returns an object with a property `initialized` instead of `initilized` -- iOS event `connectionDidConnect` returns the same properties as Android -move property `to` => `call_to` -move property `from` => `call_from` - -## Migrating Android from v1 to v2 (incoming call use FCM) -You will need to make changes both on your Twilio account using Twilio Web Console and on your react native app. -Twilio Programmable Voice Android SDK uses `FCM` since version 2.0.0.beta5. -Before you start, I strongly suggest that you read the list of Twilio changes from Android SDK v2.0.0 beta4 to beta5: -[Twilio example App: Migrating from GCM to FCM](https://github.com/twilio/voice-quickstart-android/blob/d7d4f0658e145eb94ab8f5e34f6fd17314e7ab17/README.md#migrating-from-gcm-to-fcm) - -These are all the changes required: - -- remove all the GCM related code from your `AndroidManifest.xml` and add the following code to receive `FCM` notifications -(I wasn't successful in keeping react-native-fcm working at the same time. If you know how please open an issue to share). +## Breaking changes in v4.0.0 +- Android: remove the following block from your application's `AndroidManifest.xml` ```xml - ..... - - - - - - - - - - ``` -- log into your Firebase console. Navigate to: Project settings > CLOUD MESSAGING. Copy your `Server key` -- in Twilio console add a new Push Credential, type `FCM`, fcm secret Firebase FCM `Server key` -- include in your project `google-services.json`; if you have not include it yet -- rename getIncomingCall() to getActiveCall() +- iOS: params changes for `connectionDidConnect` and `connectionDidDisconnect` + + to => call_to + from => call_from + error => err -If something doesn't work as expected or you want to make a request open an issue. +## Breaking changes in v3.0.0 + +- initWitToken returns an object with a property `initialized` instead of `initilized` +- iOS event `connectionDidConnect` returns the same properties as Android +move property `to` => `call_to` +move property `from` => `call_from` ## Help wanted! @@ -220,8 +199,8 @@ public class MainApplication extends Application implements ReactApplication { protected List getPackages() { return Arrays.asList( new MainReactPackage(), - new TwilioVoicePackage() // <---- Add the Package : by default it will ask microphone permissions - // new TwilioVoicePackage(false) // <---- pass false to handle microphone permissions in your application + new TwilioVoicePackage() // <---- Add the package + // new TwilioVoicePackage(false) // <---- pass false if you don't want to ask for microphone permissions ); } }; @@ -277,7 +256,7 @@ 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' | 'RECONNECTING' | 'DISCONNECTED' | 'CANCELLED', // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // } @@ -297,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' | 'RECONNECTING' | 'DISCONNECTED' | 'CANCELLED', // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // err?: string, @@ -321,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' | 'RECONNECTING' | 'DISCONNECTED' | 'CANCELLED', // call_from: string, // "+441234567890" // call_to: string, // "client:bob" // } diff --git a/android/build.gradle b/android/build.gradle index 878fc179..13d135fb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,11 +2,16 @@ buildscript { repositories { + maven { + url 'https://maven.google.com/' + name 'Google' + } jcenter() + google() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.1' - classpath 'com.google.gms:google-services:3.1.2' + classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.google.gms:google-services:4.2.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -15,21 +20,27 @@ buildscript { allprojects { repositories { - jcenter() maven { - url "https://maven.google.com" + url 'https://maven.google.com/' + name 'Google' } + jcenter() } } apply plugin: 'com.android.library' +def DEFAULT_COMPILE_SDK_VERSION = 28 +def DEFAULT_BUILD_TOOLS_VERSION = "28.0.3" +def DEFAULT_TARGET_SDK_VERSION = 28 +def DEFAULT_SUPPORT_LIB_VERSION = "28.0.3" + android { - compileSdkVersion 27 - buildToolsVersion "27.0.3" + compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION + buildToolsVersion rootProject.hasProperty('buildToolsVersion') ? rootProject.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION defaultConfig { minSdkVersion 16 - targetSdkVersion 27 + targetSdkVersion rootProject.hasProperty('targetSdkVersion') ? rootProject.targetSdkVersion : DEFAULT_TARGET_SDK_VERSION versionCode 1 versionName "1.0" vectorDrawables.useSupportLibrary = true @@ -43,9 +54,11 @@ android { } dependencies { + def supportLibVersion = rootProject.hasProperty('supportLibVersion') ? rootProject.supportLibVersion : DEFAULT_SUPPORT_LIB_VERSION + compile fileTree(include: ['*.jar'], dir: 'libs') - compile 'com.twilio:voice-android:2.0.7' - compile 'com.android.support:appcompat-v7:27.0.2' + compile 'com.twilio:voice-android:4+' + compile 'com.android.support:appcompat-v7:$supportLibVersion' compile 'com.facebook.react:react-native:+' compile 'com.google.firebase:firebase-messaging:17.+' testCompile 'junit:junit:4.12' diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 04e285f3..708a0dce 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip +distributionUrl=https://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index e69de29b..00000000 diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java index bace1249..11050c1d 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,12 @@ public void createHangupLocalNotification(ReactApplicationContext context, Strin } public void removeIncomingCallNotification(ReactApplicationContext context, - CallInvite callInvite, + CancelledCallInvite callInvite, int notificationId) { Log.d(TAG, "removeIncomingCallNotification"); 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/TwilioVoiceModule.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java index 774ad936..fae97b8a 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java @@ -15,6 +15,7 @@ import android.media.AudioManager; import android.os.Build; +import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; import android.support.v4.content.LocalBroadcastManager; @@ -38,11 +39,14 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; -import com.google.firebase.FirebaseApp; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; import com.google.firebase.iid.FirebaseInstanceId; +import com.google.firebase.iid.InstanceIdResult; import com.twilio.voice.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; @@ -75,8 +79,10 @@ 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_CANCEL_CALL = "com.hoxfon.react.TwilioVoice.CANCEL_CALL"; public static final String ACTION_FCM_TOKEN = "com.hoxfon.react.TwilioVoice.ACTION_FCM_TOKEN"; public static final String ACTION_MISSED_CALL = "com.hoxfon.react.TwilioVoice.MISSED_CALL"; public static final String ACTION_ANSWER_CALL = "com.hoxfon.react.TwilioVoice.ANSWER_CALL"; @@ -214,6 +220,24 @@ 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) { + Log.d(TAG, "Ringing"); + } + @Override public void onConnected(Call call) { if (BuildConfig.DEBUG) { @@ -242,6 +266,16 @@ public void onConnected(Call call) { eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params); } + @Override + public void onReconnecting(Call call, CallException callException) { + Log.d(TAG, "onReconnecting"); + } + + @Override + public void onReconnected(Call call) { + Log.d(TAG, "onReconnected"); + } + @Override public void onDisconnected(Call call, CallException error) { unsetAudioFocus(); @@ -386,7 +420,7 @@ private void handleIncomingCallIntent(Intent intent) { if (intent.getAction().equals(ACTION_INCOMING_CALL)) { activeCallInvite = intent.getParcelableExtra(INCOMING_CALL_INVITE); - if (activeCallInvite != null && (activeCallInvite.getState() == CallInvite.State.PENDING)) { + if (activeCallInvite != null) { // && (activeCallInvite.getState() == CallInvite.State.PENDING) callAccepted = false; if (BuildConfig.DEBUG) { Log.d(TAG, "handleIncomingCallIntent state = PENDING"); @@ -409,57 +443,45 @@ private void handleIncomingCallIntent(Intent intent) { params.putString("call_sid", activeCallInvite.getCallSid()); params.putString("call_from", activeCallInvite.getFrom()); params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); + // params.putString("call_state", activeCallInvite.getState().name()); eventManager.sendEvent(EVENT_DEVICE_DID_RECEIVE_INCOMING, params); } - - } else { - 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(); + } + } else if (intent.getAction().equals(ACTION_CANCEL_CALL)) { + 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 - // the call is not active yet - if (activeCall == null) { + SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); - 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); - } - } + 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, "====> END"); + Log.d(TAG, "creating a missed call"); + } + callNotificationManager.createMissedCallNotification(getReactApplicationContext(), activeCallInvite); + int appImportance = callNotificationManager.getApplicationImportance(getReactApplicationContext()); + if (appImportance != RunningAppProcessInfo.IMPORTANCE_BACKGROUND) { + WritableMap params = Arguments.createMap(); + params.putString("call_sid", activeCallInvite.getCallSid()); + params.putString("call_from", activeCallInvite.getFrom()); + params.putString("call_to", activeCallInvite.getTo()); + // params.putString("call_state", activeCallInvite.getState().name()); + 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"); @@ -494,15 +516,16 @@ public void initWithAccessToken(final String accessToken, Promise promise) { if (accessToken.equals("")) { promise.reject(new JSApplicationIllegalArgumentException("Invalid access token")); return; - } - + } + if(!checkPermissionForMicrophone()) { - promise.reject(new AssertionException("Can't init without microphone permission")); - } + promise.reject(new AssertionException("Allow microphone permission")); + return; + } TwilioVoiceModule.this.accessToken = accessToken; if (BuildConfig.DEBUG) { - Log.d(TAG, "initWithAccessToken ACTION_FCM_TOKEN"); + Log.d(TAG, "initWithAccessToken"); } registerForCallInvites(); WritableMap params = Arguments.createMap(); @@ -512,7 +535,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 state: "+ callInvite.getState()); } if (callInvite != null && callInvite.getCallSid() != null) { // remove incoming call notification @@ -533,20 +556,28 @@ private void clearIncomingNotification(CallInvite callInvite) { * If a valid google-services.json has not been provided or the FirebaseInstanceId has not been * initialized the fcmToken will be null. * - * In the case where the FirebaseInstanceId has not yet been initialized the - * VoiceFirebaseInstanceIDService.onTokenRefresh should result in a LocalBroadcast to this - * activity which will attempt registerForCallInvites again. - * */ private void registerForCallInvites() { - FirebaseApp.initializeApp(getReactApplicationContext()); - final String fcmToken = FirebaseInstanceId.getInstance().getToken(); - if (fcmToken != null) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "Registering with FCM"); - } - Voice.register(getReactApplicationContext(), accessToken, Voice.RegistrationChannel.FCM, fcmToken, registrationListener); - } + FirebaseInstanceId.getInstance().getInstanceId() + .addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + if (!task.isSuccessful()) { + Log.w(TAG, "getInstanceId failed", task.getException()); + return; + } + + // Get new Instance ID token + String fcmToken = task.getResult().getToken(); + if (fcmToken != null) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Registering with FCM"); + } + Voice.register(getReactApplicationContext(), accessToken, Voice.RegistrationChannel.FCM, fcmToken, registrationListener); + } + } + }); + } @ReactMethod @@ -554,26 +585,26 @@ public void accept() { callAccepted = true; SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); if (activeCallInvite != null){ - if (activeCallInvite.getState() == CallInvite.State.PENDING) { + // 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); - } + // } 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); + // } } else { eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, null); } @@ -588,7 +619,7 @@ public void reject() { params.putString("call_sid", activeCallInvite.getCallSid()); params.putString("call_from", activeCallInvite.getFrom()); params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); + // params.putString("call_state", activeCallInvite.getState().name()); activeCallInvite.reject(getReactApplicationContext()); clearIncomingNotification(activeCallInvite); } @@ -604,7 +635,7 @@ public void ignore() { params.putString("call_sid", activeCallInvite.getCallSid()); params.putString("call_from", activeCallInvite.getFrom()); params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", activeCallInvite.getState().name()); + // params.putString("call_state", activeCallInvite.getState().name()); clearIncomingNotification(activeCallInvite); } eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); @@ -661,7 +692,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 @@ -702,13 +737,13 @@ public void getActiveCall(Promise promise) { } if (activeCallInvite != null) { if (BuildConfig.DEBUG) { - Log.d(TAG, "Active call invite found state = "+activeCallInvite.getState()); + // 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()); + // params.putString("call_state", activeCallInvite.getState().name()); promise.resolve(params); return; } @@ -778,14 +813,15 @@ private boolean checkPermissionForMicrophone() { } private void requestPermissionForMicrophone() { - if (getCurrentActivity() != null) { - if (ActivityCompat.shouldShowRequestPermissionRationale(getCurrentActivity(), Manifest.permission.RECORD_AUDIO)) { - // Snackbar.make(coordinatorLayout, - // "Microphone permissions needed. Please allow in your application settings.", - // SNACKBAR_DURATION).show(); - } else { - ActivityCompat.requestPermissions(getCurrentActivity(), new String[]{Manifest.permission.RECORD_AUDIO}, MIC_PERMISSION_REQUEST_CODE); - } + if (getCurrentActivity() == null) { + return; + } + if (ActivityCompat.shouldShowRequestPermissionRationale(getCurrentActivity(), Manifest.permission.RECORD_AUDIO)) { +// Snackbar.make(coordinatorLayout, +// "Microphone permissions needed. Please allow in your application settings.", +// SNACKBAR_DURATION).show(); + } else { + ActivityCompat.requestPermissions(getCurrentActivity(), new String[]{Manifest.permission.RECORD_AUDIO}, MIC_PERMISSION_REQUEST_CODE); } } } diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseInstanceIDService.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseInstanceIDService.java deleted file mode 100644 index 9154661c..00000000 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseInstanceIDService.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.hoxfon.react.RNTwilioVoice.fcm; - -import android.content.Intent; -import android.support.v4.content.LocalBroadcastManager; -import android.util.Log; - -import com.google.firebase.iid.FirebaseInstanceId; -import com.google.firebase.iid.FirebaseInstanceIdService; -import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_FCM_TOKEN; -import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG; - -public class VoiceFirebaseInstanceIDService extends FirebaseInstanceIdService { - - /** - * Called if InstanceID token is updated. This may occur if the security of - * the previous token had been compromised. Note that this is called when the InstanceID token - * is initially generated so this is where you would retrieve the token. - */ - // [START refresh_token] - @Override - public void onTokenRefresh() { - // Get updated InstanceID token. - String refreshedToken = FirebaseInstanceId.getInstance().getToken(); - Log.d(TAG, "Refreshed token: " + refreshedToken); - - // Notify Activity of FCM token - Intent intent = new Intent(ACTION_FCM_TOKEN); - LocalBroadcastManager.getInstance(this).sendBroadcast(intent); - } - // [END refresh_token] - - /** - * Persist token to third-party servers. - * - * Modify this method to associate the user's FCM InstanceID token with any server-side account - * maintained by your application. - * - * @param token The new token. - */ - private void sendRegistrationToServer(String token) { - // TODO: Implement this method to send token to your app server. - } -} diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/fcm/VoiceFirebaseMessagingService.java index b50facdc..d0a290a9 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 @@ -4,7 +4,6 @@ import android.app.ActivityManager; import android.content.Intent; -import android.os.Build; import android.os.Handler; import android.os.Looper; import android.support.v4.content.LocalBroadcastManager; @@ -20,7 +19,7 @@ import com.hoxfon.react.RNTwilioVoice.BuildConfig; import com.hoxfon.react.RNTwilioVoice.CallNotificationManager; import com.twilio.voice.CallInvite; -import com.twilio.voice.MessageException; +import com.twilio.voice.CancelledCallInvite; import com.twilio.voice.MessageListener; import com.twilio.voice.Voice; @@ -28,8 +27,11 @@ import java.util.Random; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_FCM_TOKEN; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_INCOMING_CALL; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_CANCEL_CALL; 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; @@ -43,6 +45,15 @@ public void onCreate() { callNotificationManager = new CallNotificationManager(); } + @Override + public void onNewToken(String token) { + Log.d(TAG, "Refreshed token: " + token); + + // Notify Activity of FCM token + Intent intent = new Intent(ACTION_FCM_TOKEN); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + /** * Called when message is received. * @@ -62,8 +73,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) { @@ -117,10 +127,23 @@ 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, "Error handling FCM message"); + } } // Check if message contains a notification payload. @@ -152,6 +175,15 @@ private void sendIncomingCallMessageToActivity( LocalBroadcastManager.getInstance(context).sendBroadcast(intent); } + /* + * Send the CancelledCallInvite to the VoiceActivity + */ + private void sendCancelledCallInviteToActivity(CancelledCallInvite cancelledCallInvite) { + Intent intent = new Intent(ACTION_CANCEL_CALL); + intent.putExtra(CANCELLED_CALL_INVITE, cancelledCallInvite); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + } + /* * Show the notification in the Android notification drawer */ @@ -161,11 +193,12 @@ private void showNotification(ReactApplicationContext context, 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); - } + callNotificationManager.createIncomingCallNotification(context, callInvite, notificationId, launchIntent); } + + private void cancelNotification(ReactApplicationContext context, CancelledCallInvite cancelledCallInvite) { + SoundPoolManager.getInstance((this)).stopRinging(); + callNotificationManager.removeIncomingCallNotification(context, cancelledCallInvite, 0); + } + } diff --git a/ios/RNTwilioVoice/RNTwilioVoice.m b/ios/RNTwilioVoice/RNTwilioVoice.m index 0e0a6bfc..57c43c54 100644 --- a/ios/RNTwilioVoice/RNTwilioVoice.m +++ b/ios/RNTwilioVoice/RNTwilioVoice.m @@ -99,7 +99,7 @@ - (void)dealloc { device.proximityMonitoringEnabled = YES; if (self.call && self.call.state == TVOCallStateConnected) { - [self.call disconnect]; + [self performEndCallActionWithUUID:self.call.uuid]; } else { NSUUID *uuid = [NSUUID UUID]; NSString *handle = [params valueForKey:@"To"]; @@ -122,14 +122,14 @@ - (void)dealloc { [self toggleAudioRoute:speaker]; } -RCT_EXPORT_METHOD(sendDigits: (NSString *)digits){ +RCT_EXPORT_METHOD(sendDigits: (NSString *)digits) { if (self.call && self.call.state == TVOCallStateConnected) { NSLog(@"SendDigits %@", digits); [self.call sendDigits:digits]; } } -RCT_EXPORT_METHOD(unregister){ +RCT_EXPORT_METHOD(unregister) { NSLog(@"unregister"); NSString *accessToken = [self fetchAccessToken]; @@ -148,16 +148,16 @@ - (void)dealloc { RCT_REMAP_METHOD(getActiveCall, resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject){ + rejecter:(RCTPromiseRejectBlock)reject) { NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; if (self.callInvite) { - if (self.callInvite.callSid){ + if (self.callInvite.callSid) { [params setObject:self.callInvite.callSid forKey:@"call_sid"]; } - if (self.callInvite.from){ + if (self.callInvite.from) { [params setObject:self.callInvite.from forKey:@"from"]; } - if (self.callInvite.to){ + if (self.callInvite.to) { [params setObject:self.callInvite.to forKey:@"to"]; } if (self.callInvite.state == TVOCallInviteStatePending) { @@ -172,10 +172,10 @@ - (void)dealloc { if (self.call.sid) { [params setObject:self.call.sid forKey:@"call_sid"]; } - if (self.call.to){ + if (self.call.to) { [params setObject:self.call.to forKey:@"call_to"]; } - if (self.call.from){ + if (self.call.from) { [params setObject:self.call.from forKey:@"call_from"]; } if (self.call.state == TVOCallStateConnected) { @@ -294,15 +294,15 @@ - (void)handleCallInviteCanceled:(TVOCallInvite *)callInvite { [self performEndCallActionWithUUID:callInvite.uuid]; NSMutableDictionary *params = [[NSMutableDictionary alloc] init]; - if (self.callInvite.callSid){ + if (self.callInvite.callSid) { [params setObject:self.callInvite.callSid forKey:@"call_sid"]; } - if (self.callInvite.from){ - [params setObject:self.callInvite.from forKey:@"from"]; + if (self.callInvite.from) { + [params setObject:self.callInvite.from forKey:@"call_from"]; } - if (self.callInvite.to){ - [params setObject:self.callInvite.to forKey:@"to"]; + if (self.callInvite.to) { + [params setObject:self.callInvite.to forKey:@"call_to"]; } if (self.callInvite.state == TVOCallInviteStateCanceled) { [params setObject:StateDisconnected forKey:@"call_state"]; @@ -332,11 +332,11 @@ - (void)callDidConnect:(TVOCall *)call { [callParams setObject:StateConnected forKey:@"call_state"]; } - if (call.from){ - [callParams setObject:call.from forKey:@"from"]; + if (call.from) { + [callParams setObject:call.from forKey:@"call_from"]; } - if (call.to){ - [callParams setObject:call.to forKey:@"to"]; + if (call.to) { + [callParams setObject:call.to forKey:@"call_to"]; } [self sendEventWithName:@"connectionDidConnect" body:callParams]; } @@ -352,7 +352,6 @@ - (void)call:(TVOCall *)call didFailToConnectWithError:(NSError *)error { - (void)call:(TVOCall *)call didDisconnectWithError:(NSError *)error { NSLog(@"Call disconnected with error: %@", error); - [self performEndCallActionWithUUID:call.uuid]; [self callDisconnected:error]; } @@ -363,15 +362,15 @@ - (void)callDisconnected:(NSError *)error { if (error.localizedFailureReason) { errMsg = [error localizedFailureReason]; } - [params setObject:errMsg forKey:@"error"]; + [params setObject:errMsg forKey:@"err"]; } if (self.call.sid) { [params setObject:self.call.sid forKey:@"call_sid"]; } - if (self.call.to){ + if (self.call.to) { [params setObject:self.call.to forKey:@"call_to"]; } - if (self.call.from){ + if (self.call.from) { [params setObject:self.call.from forKey:@"call_from"]; } if (self.call.state == TVOCallStateDisconnected) { diff --git a/package.json b/package.json index 02d0d67b..3936971a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-twilio-programmable-voice", - "version": "3.21.1", + "version": "4.0.0", "description": "React Native wrapper for Twilio Programmable Voice SDK", "main": "index.js", "scripts": {