diff --git a/README.md b/README.md index bc38fe13..a151a5bf 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # react-native-twilio-programmable-voice -This is a React Native wrapper for Twilio Programmable Voice SDK that lets you make and receive calls from your ReactNatvie App. This module is not curated nor maintained, but inspired by Twilio. +This is a React Native wrapper for Twilio Programmable Voice SDK that lets you make and receive calls from your React Native App. This module is not affiliated with or maintained by the Twilio team. This is maintained by contributions from the community. # Twilio Programmable Voice SDK -- Android 2.1.0 (bundled within this library) +- Android 5.0.2 (bundled within this library) - iOS 2.1.0 (specified by the app's own podfile) ## Breaking changes in v4.0.0 @@ -45,7 +45,9 @@ ReactNative success is directly linked to its module ecosystem. One way to make ## Installation Before starting, we recommend you get familiar with [Twilio Programmable Voice SDK](https://www.twilio.com/docs/api/voice-sdk). -It's easier to integrate this module into your react-native app if you follow the Quick start tutorial from Twilio, because it makes very clear which setup steps are required. +It's easier to integrate this module into your react-native app if you follow the Quick start tutorial from Twilio, because it makes very clear which setup steps are required. On RN 0.60+, this module can be auto-linked (Android still requires FCM setup below). + +### Manual Linking ``` @@ -120,7 +122,7 @@ It contains keys and settings for all your applications under Firebase. This lib buildscript { ... dependencies { - classpath 'com.google.gms:google-services:3.1.2' + classpath 'com.google.gms:google-services:3.1.2' // use a newer version } } @@ -128,8 +130,9 @@ buildscript { dependencies { ... + // on React Native 0.60+, this module can be auto-linked and doesn't need a manual entry here - compile project(':react-native-twilio-programmable-voice') + implementation project(':react-native-twilio-programmable-voice') } // this plugin looks for google-services.json in your project @@ -156,22 +159,12 @@ In your `AndroidManifest.xml` - - - - - - - - ..... ``` -In `android/settings.gradle` +In `android/settings.gradle` (not necessary if auto-linking on RN 0.60+) ```gradle ... @@ -180,7 +173,7 @@ include ':react-native-twilio-programmable-voice' project(':react-native-twilio-programmable-voice').projectDir = file('../node_modules/react-native-twilio-programmable-voice/android') ``` -Register module (in `MainApplication.java`) +Register module (in `MainApplication.java`) (not necessary if auto-linking on RN 0.60+ unless you want to control microphone permission) ```java import com.hoxfon.react.RNTwilioVoice.TwilioVoicePackage; // <--- Import Package diff --git a/android/build.gradle b/android/build.gradle index ce930d69..f0a45336 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,7 +4,6 @@ buildscript { repositories { google() jcenter() - google() } dependencies { classpath 'com.android.tools.build:gradle:3.5.2' @@ -51,7 +50,7 @@ dependencies { def supportLibVersion = rootProject.hasProperty('supportLibVersion') ? rootProject.supportLibVersion : DEFAULT_SUPPORT_LIB_VERSION implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.twilio:voice-android:4.5.0' + implementation 'com.twilio:voice-android:5.0.2' implementation "com.android.support:appcompat-v7:$supportLibVersion" implementation 'com.facebook.react:react-native:+' implementation 'com.google.firebase:firebase-messaging:17.+' diff --git a/android/gradle.properties b/android/gradle.properties index aac7c9b4..af6dcbe4 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -15,3 +15,5 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java index eb861191..a68fb056 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java @@ -15,10 +15,11 @@ import android.os.Build; import android.os.Bundle; import android.service.notification.StatusBarNotification; -import androidx.core.app.NotificationCompat; import android.util.Log; import android.view.WindowManager; +import androidx.core.app.NotificationCompat; + import com.facebook.react.bridge.ReactApplicationContext; import com.twilio.voice.CallInvite; import com.twilio.voice.CancelledCallInvite; @@ -26,24 +27,23 @@ import java.util.List; import static android.content.Context.ACTIVITY_SERVICE; - -import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_ANSWER_CALL; -import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_REJECT_CALL; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_CLEAR_MISSED_CALLS_COUNT; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_HANGUP_CALL; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_INCOMING_CALL; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_MISSED_CALL; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_REJECT_CALL; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.CALL_SID_KEY; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.CLEAR_MISSED_CALLS_NOTIFICATION_ID; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.HANGUP_NOTIFICATION_ID; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_INVITE; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_NOTIFICATION_ID; -import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.NOTIFICATION_TYPE; -import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.CALL_SID_KEY; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_NOTIFICATION_PREFIX; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.MISSED_CALLS_GROUP; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.MISSED_CALLS_NOTIFICATION_ID; -import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.HANGUP_NOTIFICATION_ID; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.NOTIFICATION_TYPE; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.PREFERENCE_KEY; -import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_CLEAR_MISSED_CALLS_COUNT; -import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.CLEAR_MISSED_CALLS_NOTIFICATION_ID; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG; public class CallNotificationManager { @@ -52,7 +52,8 @@ public class CallNotificationManager { private NotificationCompat.InboxStyle inboxStyle = new NotificationCompat.InboxStyle(); - public CallNotificationManager() {} + public CallNotificationManager() { + } public int getApplicationImportance(ReactApplicationContext context) { ActivityManager activityManager = (ActivityManager) context.getSystemService(ACTIVITY_SERVICE); @@ -116,10 +117,9 @@ public Intent getLaunchIntent(ReactApplicationContext context, public void createIncomingCallNotification(ReactApplicationContext context, CallInvite callInvite, int notificationId, - Intent launchIntent) - { + Intent launchIntent) { if (BuildConfig.DEBUG) { - Log.d(TAG, "createIncomingCallNotification intent "+launchIntent.getFlags()); + Log.d(TAG, "createIncomingCallNotification intent " + launchIntent.getFlags()); } PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT); @@ -180,7 +180,7 @@ public void createIncomingCallNotification(ReactApplicationContext context, notificationBuilder.addAction(R.drawable.ic_call_white_24dp, "ANSWER", pendingAnswerIntent); notificationManager.notify(notificationId, notificationBuilder.build()); - TwilioVoiceModule.callNotificationMap.put(INCOMING_NOTIFICATION_PREFIX+callInvite.getCallSid(), notificationId); + TwilioVoiceModule.callNotificationMap.put(INCOMING_NOTIFICATION_PREFIX + callInvite.getCallSid(), notificationId); } public void initCallNotificationsChannel(NotificationManager notificationManager) { @@ -246,7 +246,7 @@ public void createMissedCallNotification(ReactApplicationContext context, CallIn } else { inboxStyle.setBigContentTitle(String.valueOf(missedCalls) + " missed calls"); } - inboxStyle.addLine("from: " +callInvite.getFrom()); + inboxStyle.addLine("from: " + callInvite.getFrom()); sharedPrefEditor.putInt(MISSED_CALLS_GROUP, missedCalls); sharedPrefEditor.commit(); @@ -334,7 +334,7 @@ public void removeIncomingCallNotification(ReactApplicationContext context, if (notificationId != 0) { notificationManager.cancel(notificationId); } else if (callInvite != null) { - String notificationKey = INCOMING_NOTIFICATION_PREFIX+callInvite.getCallSid(); + String notificationKey = INCOMING_NOTIFICATION_PREFIX + callInvite.getCallSid(); if (TwilioVoiceModule.callNotificationMap.containsKey(notificationKey)) { notificationId = TwilioVoiceModule.callNotificationMap.get(notificationKey); notificationManager.cancel(notificationId); 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..5ba23040 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java @@ -1,8 +1,9 @@ package com.hoxfon.react.RNTwilioVoice; -import androidx.annotation.Nullable; import android.util.Log; +import androidx.annotation.Nullable; + import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; @@ -11,16 +12,14 @@ public class EventManager { - private ReactApplicationContext mContext; - public static final String EVENT_PROXIMITY = "proximity"; public static final String EVENT_WIRED_HEADSET = "wiredHeadset"; - public static final String EVENT_DEVICE_READY = "deviceReady"; public static final String EVENT_DEVICE_NOT_READY = "deviceNotReady"; 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"; + private ReactApplicationContext mContext; public EventManager(ReactApplicationContext context) { mContext = context; @@ -28,12 +27,12 @@ public EventManager(ReactApplicationContext context) { public void sendEvent(String eventName, @Nullable WritableMap params) { if (BuildConfig.DEBUG) { - Log.d(TAG, "sendEvent "+eventName+" params "+params); + Log.d(TAG, "sendEvent " + eventName + " params " + params); } if (mContext.hasActiveCatalystInstance()) { mContext - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit(eventName, params); + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(eventName, params); } else { if (BuildConfig.DEBUG) { Log.d(TAG, "failed Catalyst instance not active"); diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/HeadsetManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/HeadsetManager.java index f75dad2b..cdfcab79 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/HeadsetManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/HeadsetManager.java @@ -16,11 +16,10 @@ public class HeadsetManager { - private BroadcastReceiver wiredHeadsetReceiver; private static final String ACTION_HEADSET_PLUG = (android.os.Build.VERSION.SDK_INT >= 21) - ? AudioManager.ACTION_HEADSET_PLUG - : Intent.ACTION_HEADSET_PLUG; - + ? AudioManager.ACTION_HEADSET_PLUG + : Intent.ACTION_HEADSET_PLUG; + private BroadcastReceiver wiredHeadsetReceiver; private EventManager eventManager; public HeadsetManager(EventManager em) { diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/ProximityManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/ProximityManager.java index 69575f1e..997b586c 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/ProximityManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/ProximityManager.java @@ -151,9 +151,9 @@ public void startProximitySensor() { // SensorManager.SENSOR_DELAY_UI(60 ms), // SensorManager.SENSOR_DELAY_NORMAL(200 ms) sensorManager.registerListener( - proximityListener, - proximitySensor, - SensorManager.SENSOR_DELAY_NORMAL + proximityListener, + proximitySensor, + SensorManager.SENSOR_DELAY_NORMAL ); } } diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/SoundPoolManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/SoundPoolManager.java index cda96606..08896248 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/SoundPoolManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/SoundPoolManager.java @@ -7,8 +7,8 @@ public class SoundPoolManager { - private boolean playing = false; private static SoundPoolManager instance; + private boolean playing = false; private Ringtone ringtone = null; private SoundPoolManager(Context 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 cfcdf6c7..9eee6bd0 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java @@ -14,31 +14,28 @@ import android.media.AudioFocusRequest; import android.media.AudioManager; import android.os.Build; +import android.util.Log; +import android.view.Window; +import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import android.util.Log; -import android.view.Window; -import android.view.WindowManager; -import com.facebook.react.bridge.JSApplicationIllegalArgumentException; +import com.facebook.react.bridge.ActivityEventListener; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.AssertionException; +import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.LifecycleEventListener; +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.ReadableMap; - -import com.facebook.react.bridge.ActivityEventListener; -import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReadableMapKeySetIterator; import com.facebook.react.bridge.ReadableType; import com.facebook.react.bridge.WritableMap; - -import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.Promise; -import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReactMethod; - import com.google.android.gms.tasks.OnCompleteListener; import com.google.android.gms.tasks.Task; import com.google.firebase.iid.FirebaseInstanceId; @@ -63,68 +60,53 @@ public class TwilioVoiceModule extends ReactContextBaseJavaModule implements ActivityEventListener, LifecycleEventListener { - public static String TAG = "RNTwilioVoice"; - - private static final int MIC_PERMISSION_REQUEST_CODE = 1; - - private AudioManager audioManager; - private int originalAudioMode = AudioManager.MODE_NORMAL; - - private boolean isReceiverRegistered = false; - private VoiceBroadcastReceiver voiceBroadcastReceiver; - - // Empty HashMap, contains parameters for the Outbound call - private HashMap twiMLParams = new HashMap<>(); - - public static final String INCOMING_CALL_INVITE = "INCOMING_CALL_INVITE"; + 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 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"; - 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 = "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"; + 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_CLEAR_MISSED_CALLS_COUNT = "com.hoxfon.react.TwilioVoice.CLEAR_MISSED_CALLS_COUNT"; - public static final String CALL_SID_KEY = "CALL_SID"; public static final String INCOMING_NOTIFICATION_PREFIX = "Incoming_"; public static final String MISSED_CALLS_GROUP = "MISSED_CALLS"; public static final int MISSED_CALLS_NOTIFICATION_ID = 1; public static final int HANGUP_NOTIFICATION_ID = 11; public static final int CLEAR_MISSED_CALLS_NOTIFICATION_ID = 21; - public static final String PREFERENCE_KEY = "com.hoxfon.react.TwilioVoice.PREFERENCE_FILE_KEY"; - + private static final int MIC_PERMISSION_REQUEST_CODE = 1; + public static String TAG = "RNTwilioVoice"; + static Map callNotificationMap; + private AudioManager audioManager; + private int originalAudioMode = AudioManager.MODE_NORMAL; + private boolean isReceiverRegistered = false; + private VoiceBroadcastReceiver voiceBroadcastReceiver; + // Empty HashMap, contains parameters for the Outbound call + private HashMap twiMLParams = new HashMap<>(); private NotificationManager notificationManager; private CallNotificationManager callNotificationManager; private ProximityManager proximityManager; - private String accessToken; - private String toNumber = ""; private String toName = ""; - - static Map callNotificationMap; - - private RegistrationListener registrationListener = registrationListener(); - private Call.Listener callListener = callListener(); - private CallInvite activeCallInvite; private Call activeCall; - // this variable determines when to create missed calls notifications private Boolean callAccepted = false; - private AudioFocusRequest focusRequest; private HeadsetManager headsetManager; private EventManager eventManager; + private RegistrationListener registrationListener = registrationListener(); + private int savedAudioMode = AudioManager.MODE_INVALID; + private Call.Listener callListener = callListener(); public TwilioVoiceModule(ReactApplicationContext reactContext, - boolean shouldAskForMicPermission) { + boolean shouldAskForMicPermission) { super(reactContext); if (BuildConfig.DEBUG) { Voice.setLogLevel(LogLevel.DEBUG); @@ -182,7 +164,7 @@ public void onHostPause() { public void onHostDestroy() { disconnect(); callNotificationManager.removeHangupNotification(getReactApplicationContext()); - unsetAudioFocus(); + setAudioFocus(false); } @Override @@ -241,15 +223,15 @@ public void onRinging(Call call) { @Override public void onConnected(Call call) { if (BuildConfig.DEBUG) { - Log.d(TAG, "CALL CONNECTED callListener().onConnected call state = "+call.getState()); + Log.d(TAG, "CALL CONNECTED callListener().onConnected call state = " + call.getState()); } - setAudioFocus(); + setAudioFocus(true); proximityManager.startProximitySensor(); headsetManager.startWiredHeadsetEvent(getReactApplicationContext()); WritableMap params = Arguments.createMap(); if (call != null) { - params.putString("call_sid", call.getSid()); + params.putString("call_sid", call.getSid()); params.putString("call_state", call.getState().name()); params.putString("call_from", call.getFrom()); params.putString("call_to", call.getTo()); @@ -260,8 +242,8 @@ public void onConnected(Call call) { caller = toNumber; } activeCall = call; - callNotificationManager.createHangupLocalNotification(getReactApplicationContext(), - call.getSid(), caller); + callNotificationManager.createHangupLocalNotification(getReactApplicationContext(), + call.getSid(), caller); } eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params); } @@ -278,7 +260,7 @@ public void onReconnected(Call call) { @Override public void onDisconnected(Call call, CallException error) { - unsetAudioFocus(); + setAudioFocus(false); proximityManager.stopProximitySensor(); headsetManager.stopWiredHeadsetEvent(getReactApplicationContext()); callAccepted = false; @@ -312,7 +294,7 @@ public void onDisconnected(Call call, CallException error) { @Override public void onConnectFailure(Call call, CallException error) { - unsetAudioFocus(); + setAudioFocus(false); proximityManager.stopProximitySensor(); callAccepted = false; if (BuildConfig.DEBUG) { @@ -320,7 +302,7 @@ public void onConnectFailure(Call call, CallException error) { } Log.e(TAG, String.format("CallListener onDisconnected error: %d, %s", - error.getErrorCode(), error.getMessage())); + error.getErrorCode(), error.getMessage())); WritableMap params = Arguments.createMap(); params.putString("err", error.getMessage()); @@ -438,12 +420,11 @@ private void handleIncomingCallIntent(Intent intent) { int appImportance = callNotificationManager.getApplicationImportance(getReactApplicationContext()); if (appImportance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND || appImportance == RunningAppProcessInfo.IMPORTANCE_SERVICE) { - WritableMap params = Arguments.createMap(); params.putString("call_sid", activeCallInvite.getCallSid()); params.putString("call_from", activeCallInvite.getFrom()); params.putString("call_to", activeCallInvite.getTo()); - // params.putString("call_state", activeCallInvite.getState().name()); + params.putString("call_state", "PENDING"); eventManager.sendEvent(EVENT_DEVICE_DID_RECEIVE_INCOMING, params); } } else { @@ -476,11 +457,11 @@ 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", "DISCONNECTED"); eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); } } - + clearIncomingNotification(activeCallInvite); } else if (intent.getAction().equals(ACTION_FCM_TOKEN)) { if (BuildConfig.DEBUG) { @@ -490,27 +471,6 @@ private void handleIncomingCallIntent(Intent intent) { } } - private class VoiceBroadcastReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - 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_MISSED_CALL)) { - SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences(PREFERENCE_KEY, Context.MODE_PRIVATE); - SharedPreferences.Editor sharedPrefEditor = sharedPref.edit(); - sharedPrefEditor.remove(MISSED_CALLS_GROUP); - sharedPrefEditor.commit(); - } else { - Log.e(TAG, "received broadcast unhandled action " + action); - } - } - } - @ReactMethod public void initWithAccessToken(final String accessToken, Promise promise) { if (accessToken.equals("")) { @@ -518,7 +478,7 @@ public void initWithAccessToken(final String accessToken, Promise promise) { return; } - if(!checkPermissionForMicrophone()) { + if (!checkPermissionForMicrophone()) { promise.reject(new AssertionException("Allow microphone permission")); return; } @@ -573,7 +533,7 @@ 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); } } }); @@ -583,27 +543,27 @@ public void onComplete(@NonNull Task task) { public void accept() { callAccepted = true; SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); - if (activeCallInvite != null){ + 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() 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 { eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, null); } @@ -614,11 +574,11 @@ 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()); + params.putString("call_state", "REJECTED"); activeCallInvite.reject(getReactApplicationContext()); clearIncomingNotification(activeCallInvite); } @@ -630,11 +590,11 @@ 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()); + params.putString("call_state", "BUSY"); clearIncomingNotification(activeCallInvite); } eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); @@ -642,8 +602,9 @@ public void ignore() { @ReactMethod public void connect(ReadableMap params) { + if (BuildConfig.DEBUG) { - Log.d(TAG, "connect params: "+params); + Log.d(TAG, "connect params: " + params); } WritableMap errParams = Arguments.createMap(); if (accessToken == null) { @@ -665,6 +626,7 @@ public void connect(ReadableMap params) { toName = params.getString("ToName"); } + twiMLParams.clear(); ReadableMapKeySetIterator iterator = params.keySetIterator(); @@ -692,8 +654,8 @@ public void connect(ReadableMap params) { } ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken) - .params(twiMLParams) - .build(); + .params(twiMLParams) + .build(); activeCall = Voice.connect(getReactApplicationContext(), connectOptions, callListener); } @@ -724,12 +686,12 @@ public void sendDigits(String digits) { public void getActiveCall(Promise promise) { if (activeCall != null) { if (BuildConfig.DEBUG) { - Log.d(TAG, "Active call found state = "+activeCall.getState()); + Log.d(TAG, "Active call found state = " + activeCall.getState()); } WritableMap params = Arguments.createMap(); - params.putString("call_sid", activeCall.getSid()); - params.putString("call_from", activeCall.getFrom()); - params.putString("call_to", activeCall.getTo()); + params.putString("call_sid", activeCall.getSid()); + params.putString("call_from", activeCall.getFrom()); + params.putString("call_to", activeCall.getTo()); params.putString("call_state", activeCall.getState().name()); promise.resolve(params); return; @@ -739,10 +701,11 @@ public void getActiveCall(Promise promise) { // 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_sid", activeCallInvite.getCallSid()); + params.putString("call_from", activeCallInvite.getFrom()); + params.putString("call_to", activeCallInvite.getTo()); + if (activeCall != null) + params.putString("call_state", activeCall.getState().name()); promise.resolve(params); return; } @@ -756,53 +719,46 @@ public void setSpeakerPhone(Boolean value) { audioManager.setSpeakerphoneOn(value); } - private void setAudioFocus() { - if (audioManager == null) { - return; - } - originalAudioMode = audioManager.getMode(); - // Request audio focus before making any device switch - if (Build.VERSION.SDK_INT >= 26) { - AudioAttributes playbackAttributes = new AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) - .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) - .build(); - focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) - .setAudioAttributes(playbackAttributes) - .setAcceptsDelayedFocusGain(true) - .setOnAudioFocusChangeListener(new AudioManager.OnAudioFocusChangeListener() { - @Override - public void onAudioFocusChange(int i) { } - }) - .build(); - audioManager.requestAudioFocus(focusRequest); - } else { - audioManager.requestAudioFocus( - null, - AudioManager.STREAM_VOICE_CALL, - AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE - ); - } - /* - * Start by setting MODE_IN_COMMUNICATION as default audio mode. It is - * required to be in this mode when playout and/or recording starts for - * best possible VoIP performance. Some devices have difficulties with speaker mode - * if this is not set. - */ - audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); - } - - private void unsetAudioFocus() { - if (audioManager == null) { - return; - } - audioManager.setMode(originalAudioMode); - if (Build.VERSION.SDK_INT >= 26) { - if (focusRequest != null) { - audioManager.abandonAudioFocusRequest(focusRequest); + private void setAudioFocus(boolean setFocus) { + if (audioManager != null) { + if (setFocus) { + savedAudioMode = audioManager.getMode(); + // Request audio focus before making any device switch. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + AudioAttributes playbackAttributes = new AudioAttributes.Builder() + .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION) + .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH) + .build(); + AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) + .setAudioAttributes(playbackAttributes) + .setAcceptsDelayedFocusGain(true) + .setOnAudioFocusChangeListener(new AudioManager.OnAudioFocusChangeListener() { + @Override + public void onAudioFocusChange(int i) { + } + }) + .build(); + audioManager.requestAudioFocus(focusRequest); + } else { + 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 + * required to be in this mode when playout and/or recording starts for + * best possible VoIP performance. Some devices have difficulties with speaker mode + * if this is not set. + */ + audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION); + } else { + audioManager.setMode(savedAudioMode); + audioManager.abandonAudioFocus(null); } - } else { - audioManager.abandonAudioFocus(null); } } @@ -823,4 +779,25 @@ private void requestPermissionForMicrophone() { ActivityCompat.requestPermissions(getCurrentActivity(), new String[]{Manifest.permission.RECORD_AUDIO}, MIC_PERMISSION_REQUEST_CODE); } } + + private class VoiceBroadcastReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + 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_MISSED_CALL)) { + SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences(PREFERENCE_KEY, Context.MODE_PRIVATE); + SharedPreferences.Editor sharedPrefEditor = sharedPref.edit(); + sharedPrefEditor.remove(MISSED_CALLS_GROUP); + sharedPrefEditor.commit(); + } else { + Log.e(TAG, "received broadcast unhandled action " + action); + } + } + } } diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoicePackage.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoicePackage.java index d69409d1..cbf8151b 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoicePackage.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoicePackage.java @@ -6,13 +6,14 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.uimanager.ViewManager; -import java.util.Collections; import java.util.ArrayList; +import java.util.Collections; import java.util.List; public class TwilioVoicePackage implements ReactPackage { private boolean mShouldAskForPermission; + public TwilioVoicePackage() { mShouldAskForPermission = true; } @@ -20,6 +21,7 @@ public TwilioVoicePackage() { public TwilioVoicePackage(boolean shouldAskForPermissions) { mShouldAskForPermission = shouldAskForPermissions; } + // Deprecated in RN 0.47.0 public List> createJSModules() { return Collections.emptyList(); 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 53538676..9c0fa5d8 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,23 +1,25 @@ package com.hoxfon.react.RNTwilioVoice.fcm; import android.annotation.TargetApi; - import android.app.ActivityManager; import android.content.Intent; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import androidx.localbroadcastmanager.content.LocalBroadcastManager; import android.util.Log; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + import com.facebook.react.ReactApplication; import com.facebook.react.ReactInstanceManager; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContext; - import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.RemoteMessage; import com.hoxfon.react.RNTwilioVoice.BuildConfig; import com.hoxfon.react.RNTwilioVoice.CallNotificationManager; +import com.hoxfon.react.RNTwilioVoice.SoundPoolManager; +import com.twilio.voice.CallException; import com.twilio.voice.CallInvite; import com.twilio.voice.CancelledCallInvite; import com.twilio.voice.MessageListener; @@ -26,14 +28,13 @@ import java.util.Map; import java.util.Random; -import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_CANCEL_CALL; 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_INVITE; import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_NOTIFICATION_ID; -import com.hoxfon.react.RNTwilioVoice.SoundPoolManager; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG; public class VoiceFirebaseMessagingService extends FirebaseMessagingService { @@ -54,6 +55,7 @@ public void onNewToken(String token) { LocalBroadcastManager.getInstance(this).sendBroadcast(intent); } + /** * Called when message is received. * @@ -61,8 +63,11 @@ public void onNewToken(String token) { */ @Override public void onMessageReceived(RemoteMessage remoteMessage) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Received onMessageReceived()"); Log.d(TAG, "Bundle data: " + remoteMessage.getData()); + Log.d(TAG, "From: " + remoteMessage.getFrom()); } // Check if message contains a data payload. @@ -73,7 +78,7 @@ public void onMessageReceived(RemoteMessage remoteMessage) { Random randomNumberGenerator = new Random(System.currentTimeMillis()); final int notificationId = randomNumberGenerator.nextInt(); - boolean valid = Voice.handleMessage(data, new MessageListener() { + boolean valid = Voice.handleMessage(getApplicationContext(), data, new MessageListener() { @Override public void onCallInvite(final CallInvite callInvite) { @@ -88,12 +93,12 @@ public void run() { ReactContext context = mReactInstanceManager.getCurrentReactContext(); // If it's constructed, send a notification if (context != null) { - int appImportance = callNotificationManager.getApplicationImportance((ReactApplicationContext)context); + int appImportance = callNotificationManager.getApplicationImportance((ReactApplicationContext) context); if (BuildConfig.DEBUG) { Log.d(TAG, "CONTEXT present appImportance = " + appImportance); } Intent launchIntent = callNotificationManager.getLaunchIntent( - (ReactApplicationContext)context, + (ReactApplicationContext) context, notificationId, callInvite, false, @@ -103,18 +108,18 @@ public void run() { if (appImportance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { context.startActivity(launchIntent); } - VoiceFirebaseMessagingService.this.handleIncomingCall((ReactApplicationContext)context, notificationId, callInvite, launchIntent); + VoiceFirebaseMessagingService.this.handleIncomingCall((ReactApplicationContext) context, notificationId, callInvite, launchIntent); } else { // Otherwise wait for construction, then handle the incoming call mReactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() { public void onReactContextInitialized(ReactContext context) { - int appImportance = callNotificationManager.getApplicationImportance((ReactApplicationContext)context); + int appImportance = callNotificationManager.getApplicationImportance((ReactApplicationContext) context); if (BuildConfig.DEBUG) { Log.d(TAG, "CONTEXT not present appImportance = " + appImportance); } - Intent launchIntent = callNotificationManager.getLaunchIntent((ReactApplicationContext)context, notificationId, callInvite, true, appImportance); + Intent launchIntent = callNotificationManager.getLaunchIntent((ReactApplicationContext) context, notificationId, callInvite, true, appImportance); context.startActivity(launchIntent); - VoiceFirebaseMessagingService.this.handleIncomingCall((ReactApplicationContext)context, notificationId, callInvite, launchIntent); + VoiceFirebaseMessagingService.this.handleIncomingCall((ReactApplicationContext) context, notificationId, callInvite, launchIntent); } }); if (!mReactInstanceManager.hasStartedCreatingInitialContext()) { @@ -127,15 +132,15 @@ public void onReactContextInitialized(ReactContext context) { } @Override - public void onCancelledCallInvite(final CancelledCallInvite cancelledCallInvite) { + public void onCancelledCallInvite(final CancelledCallInvite cancelledCallInvite, final CallException callException) { 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); + ReactContext context = mReactInstanceManager.getCurrentReactContext(); + VoiceFirebaseMessagingService.this.cancelNotification((ReactApplicationContext)context, cancelledCallInvite); VoiceFirebaseMessagingService.this.sendCancelledCallInviteToActivity( - cancelledCallInvite); + cancelledCallInvite); } }); } @@ -201,4 +206,5 @@ private void cancelNotification(ReactApplicationContext context, CancelledCallIn callNotificationManager.removeIncomingCallNotification(context, cancelledCallInvite, 0); } + } diff --git a/package.json b/package.json index 769eeede..98bd0ed7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-twilio-programmable-voice", - "version": "4.0.0", + "version": "5.0.0", "description": "React Native wrapper for Twilio Programmable Voice SDK", "main": "index.js", "scripts": {