diff --git a/README.md b/README.md
index e9061d65..9e30eb8e 100644
--- a/README.md
+++ b/README.md
@@ -16,12 +16,13 @@ Tested with:
The most updated branch is [feat/twilio-android-sdk-5](https://github.com/hoxfon/react-native-twilio-programmable-voice/tree/feat/twilio-android-sdk-5) which is aligned with:
-- Android 5.0.2
+- Android 5.4.2
- iOS 5.2.0
It contains breaking changes from `react-native-twilio-programmable-voice` v4, and it will be released as v5.
You can install it with:
+
```bash
# Yarn
yarn add https://github.com/hoxfon/react-native-twilio-programmable-voice#feat/twilio-android-sdk-5
@@ -53,6 +54,159 @@ Allow Android to use the built in Android telephony service to make and receive
- Android 4.5.0
- iOS 5.2.0
+### Breaking changes in v5.0.0
+
+Changes on [Android Twilio Voice SDK v5](https://www.twilio.com/docs/voice/voip-sdk/android/3x-changelog#500) are reflected in the JavaScript API, the way call invites are handled has changed and other v5 features like `audioSwitch` have been implemented.
+`setSpeakerPhone()` has been removed from Android, use selectAudioDevice(name: string) instead.
+
+#### Background incoming calls
+
+- When the app is not in foreground incoming calls result in a heads-up notification with action to "ACCEPT" and "REJECT".
+- ReactMethod `accept` does not dispatch any event. In v4 it dispatched `connectionDidDisconnect`.
+- ReactMethod `reject` dispatches a `callInviteCancelled` event instead of `connectionDidDisconnect`.
+- ReactMethod `ignore` does not dispatch any event. In v4 it dispatched `connectionDidDisconnect`.
+
+To show heads up notifications, you must add the following lines to your application's `android/app/src/main/AndroidManifest.xml`:
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+Firebase Messaging 19.0.+ is imported by this module, so there is no need to import it in your app's `bundle.gradle` file.
+
+In v4 the flow to launch the app when receiving a call was:
+
+1. the module launched the app
+2. after the React app is initialised, it always asked to the native module whether there were incoming call invites
+3. if there were any incoming call invites, the module would have sent an event to the React app with the incoming call invite parameters
+4. the Reach app would have listened to the event and would have launched the view with the appropriate incoming call answer/reject controls
+
+This loop was long and prone to race conditions. For example,when the event was sent before the React main view was completely initialised, it would not be handled at all.
+
+V5 replaces the previous flow by using `getLaunchOptions()` to pass initial properties from the native module to React, when receiving a call invite as explained here: https://reactnative.dev/docs/communication-android.
+
+The React app is launched with the initial properties `callInvite` or `call`.
+
+To handle correctly `lauchedOptions`, you must add the following blocks to your app's `MainActivity`:
+
+```java
+
+import com.hoxfon.react.RNTwilioVoice.TwilioModule;
+...
+
+public class MainActivity extends ReactActivity {
+
+ @Override
+ protected ReactActivityDelegate createReactActivityDelegate() {
+ return new ReactActivityDelegate(this, getMainComponentName()) {
+ @Override
+ protected ReactRootView createRootView() {
+ return new RNGestureHandlerEnabledRootView(MainActivity.this);
+ }
+ @Override
+ protected Bundle getLaunchOptions() {
+ return TwilioModule.getActivityLaunchOption(this.getPlainActivity().getIntent());
+ }
+ };
+ }
+
+ // ...
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+ setShowWhenLocked(true);
+ setTurnScreenOn(true);
+ }
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+ | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ }
+
+ // ...
+}
+```
+
+#### Audio Switch
+
+Access to native Twilio SDK AudioSwitch module for Android has been added to the JavaScript API:
+
+```javascript
+// getAudioDevices returns all audio devices connected
+// {
+// "Speakerphone": false,
+// "Earnpiece": true, // true indicates the selected device
+// }
+getAudioDevices()
+
+// getSelectedAudioDevice returns the selected audio device
+getSelectedAudioDevice()
+
+// selectAudioDevice selects the passed audio device for the current active call
+selectAudioDevice(name: string)
+```
+
+#### Event deviceDidReceiveIncoming
+
+When a call invite is received, the [SHAKEN/STIR](https://www.twilio.com/docs/voice/trusted-calling-using-shakenstir) `caller_verification` field has been added to the list of params for `deviceDidReceiveIncoming`. Values are: `verified`, `unverified`, `unknown`.
+
+## ICE
+
+See https://www.twilio.com/docs/stun-turn
+
+```bash
+curl -X POST https://api.twilio.com/2010-04-01/Accounts/ACb0b56ae3bf07ce4045620249c3c90b40/Tokens.json \
+-u ACb0b56ae3bf07ce4045620249c3c90b40:f5c84f06e5c02b55fa61696244a17c84
+```
+
+```java
+Set iceServers = new HashSet<>();
+// server URLs returned by calling the Twilio Rest API to generate a new token
+iceServers.add(new IceServer("stun:global.stun.twilio.com:3478?transport=udp"));
+iceServers.add(new IceServer("turn:global.turn.twilio.com:3478?transport=udp","8e6467be547b969ad913f7bdcfb73e411b35f648bd19f2c1cb4161b4d4a067be","n8zwmkgjIOphHN93L/aQxnkUp1xJwrZVLKc/RXL0ZpM="));
+iceServers.add(new IceServer("turn:global.turn.twilio.com:3478?transport=tcp","8e6467be547b969ad913f7bdcfb73e411b35f648bd19f2c1cb4161b4d4a067be","n8zwmkgjIOphHN93L/aQxnkUp1xJwrZVLKc/RXL0ZpM="));
+iceServers.add(new IceServer("turn:global.turn.twilio.com:443?transport=tcp","8e6467be547b969ad913f7bdcfb73e411b35f648bd19f2c1cb4161b4d4a067be","n8zwmkgjIOphHN93L/aQxnkUp1xJwrZVLKc/RXL0ZpM="));
+
+IceOptions iceOptions = new IceOptions.Builder()
+ .iceServers(iceServers)
+ .build();
+
+ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken)
+ .iceOptions(iceOptions)
+ .enableDscp(true)
+ .params(twiMLParams)
+ .build();
+```
+
### Breaking changes in v4.0.0
The module implements [react-native autolinking](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md) as many other native libraries > react-native 0.60.0, therefore it doesn't need to be linked manually.
@@ -192,7 +346,8 @@ apply plugin: 'com.google.gms.google-services'
+ android:name="com.hoxfon.react.RNTwilioVoice.fcm.VoiceFirebaseMessagingService"
+ android:stopWithTask="false">
@@ -410,7 +565,7 @@ TwilioVoice.getCallInvite()
}
})
-// Unregister device with Twilio (iOS only)
+// Unregister device with Twilio
TwilioVoice.unregister()
```
diff --git a/android/build.gradle b/android/build.gradle
index fe752fa4..f0033873 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,13 +1,29 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
+
+ ext.versions = [
+ 'java' : JavaVersion.VERSION_1_8,
+ 'androidGradlePlugin': '4.1.3',
+ 'googleServices' : '4.3.4',
+ 'compileSdk' : 31,
+ 'buildTools' : '30.0.2',
+ 'minSdk' : 23,
+ 'targetSdk' : 30,
+ 'firebase' : '19.0.+',
+ 'voiceAndroid' : '6.0.0',
+ 'audioSwitch' : '1.1.2',
+ 'androidxLifecycle' : '2.2.0',
+ ]
+
repositories {
google()
jcenter()
+ mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:3.5.4'
- classpath 'com.google.gms:google-services:4.3.4'
+ classpath "com.android.tools.build:gradle:${versions.androidGradlePlugin}"
+ classpath "com.google.gms:google-services:${versions.googleServices}"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -23,22 +39,16 @@ allprojects {
apply plugin: 'com.android.library'
-def DEFAULT_MIN_SDK_VERSION = 23
-def DEFAULT_COMPILE_SDK_VERSION = 30
-def DEFAULT_BUILD_TOOLS_VERSION = "29.0.3"
-def DEFAULT_TARGET_SDK_VERSION = 29
-def DEFAULT_SUPPORT_LIB_VERSION = "29.0.3"
-
android {
- compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : DEFAULT_COMPILE_SDK_VERSION
- buildToolsVersion rootProject.hasProperty('buildToolsVersion') ? rootProject.buildToolsVersion : DEFAULT_BUILD_TOOLS_VERSION
+ compileSdkVersion rootProject.hasProperty('compileSdkVersion') ? rootProject.compileSdkVersion : versions.compileSdk
+ buildToolsVersion rootProject.hasProperty('buildToolsVersion') ? rootProject.buildToolsVersion : versions.buildTools
compileOptions {
- sourceCompatibility 1.8
- targetCompatibility 1.8
+ sourceCompatibility versions.java
+ targetCompatibility versions.java
}
defaultConfig {
- minSdkVersion rootProject.hasProperty('minSdkVersion') ? rootProject.minSdkVersion : DEFAULT_MIN_SDK_VERSION
- targetSdkVersion rootProject.hasProperty('targetSdkVersion') ? rootProject.targetSdkVersion : DEFAULT_TARGET_SDK_VERSION
+ minSdkVersion rootProject.hasProperty('minSdkVersion') ? rootProject.minSdkVersion : versions.minSdk
+ targetSdkVersion rootProject.hasProperty('targetSdkVersion') ? rootProject.targetSdkVersion : versions.targetSdk
versionCode 1
versionName "1.0"
vectorDrawables.useSupportLibrary = true
@@ -52,12 +62,11 @@ android {
}
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.android.support:appcompat-v7:$supportLibVersion"
- implementation 'com.facebook.react:react-native:+'
- implementation 'com.google.firebase:firebase-messaging:17.6.+'
- testImplementation 'junit:junit:4.12'
+ implementation "com.twilio:audioswitch:${versions.audioSwitch}"
+ implementation "com.twilio:voice-android:${versions.voiceAndroid}"
+ implementation "com.facebook.react:react-native:+"
+ implementation "com.google.firebase:firebase-messaging:${versions.firebase}"
+ implementation "androidx.lifecycle:lifecycle-extensions:${versions.androidxLifecycle}"
+ testImplementation "junit:junit:4.12"
}
diff --git a/android/gradle.properties b/android/gradle.properties
index af6dcbe4..a066d344 100644
--- a/android/gradle.properties
+++ b/android/gradle.properties
@@ -16,4 +16,4 @@ org.gradle.jvmargs=-Xmx1536m
# 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
+android.enableJetifier=true
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index f6405f2b..a24470fe 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-5.4.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
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 af4e9175..44ed14bc 100644
--- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java
+++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java
@@ -1,5 +1,6 @@
package com.hoxfon.react.RNTwilioVoice;
+import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.app.Notification;
import android.app.NotificationChannel;
@@ -14,37 +15,17 @@
import android.graphics.Color;
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;
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_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.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.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;
-
public class CallNotificationManager {
@@ -72,7 +53,7 @@ public int getApplicationImportance(ReactApplicationContext context) {
return 0;
}
- public Class getMainActivityClass(ReactApplicationContext context) {
+ public static Class getMainActivityClass(Context context) {
String packageName = context.getPackageName();
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
String className = launchIntent.getComponent().getClassName();
@@ -84,170 +65,69 @@ public Class getMainActivityClass(ReactApplicationContext context) {
}
}
- public Intent getLaunchIntent(ReactApplicationContext context,
- int notificationId,
- CallInvite callInvite,
- Boolean shouldStartNewTask,
- int appImportance
- ) {
- Intent launchIntent = new Intent(context, getMainActivityClass(context));
-
- int launchFlag = Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP;
- if (shouldStartNewTask || appImportance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
- launchFlag = Intent.FLAG_ACTIVITY_NEW_TASK;
- }
-
- launchIntent.setAction(ACTION_INCOMING_CALL)
- .putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId)
- .addFlags(
- launchFlag +
- WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED +
- WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD +
- WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON +
- WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
- );
-
- if (callInvite != null) {
- launchIntent.putExtra(INCOMING_CALL_INVITE, callInvite);
- }
- return launchIntent;
- }
-
- public void createIncomingCallNotification(ReactApplicationContext context,
- CallInvite callInvite,
- int notificationId,
- Intent launchIntent)
- {
+ public void createMissedCallNotification(ReactApplicationContext context, String callSid, String callFrom) {
if (BuildConfig.DEBUG) {
- Log.d(TAG, "createIncomingCallNotification intent "+launchIntent.getFlags());
+ Log.d(TAG, "createMissedCallNotification()");
}
- PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
-
- /*
- * Pass the notification id and call sid to use as an identifier to cancel the
- * notification later
- */
- Bundle extras = new Bundle();
- extras.putInt(INCOMING_CALL_NOTIFICATION_ID, notificationId);
- extras.putString(CALL_SID_KEY, callInvite.getCallSid());
- extras.putString(NOTIFICATION_TYPE, ACTION_INCOMING_CALL);
- /*
- * Create the notification shown in the notification drawer
- */
- initCallNotificationsChannel(notificationManager);
-
- NotificationCompat.Builder notificationBuilder =
- new NotificationCompat.Builder(context, VOICE_CHANNEL)
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setCategory(NotificationCompat.CATEGORY_CALL)
- .setSmallIcon(R.drawable.ic_call_white_24dp)
- .setContentTitle("Incoming call")
- .setContentText(callInvite.getFrom() + " is calling")
- .setOngoing(true)
- .setAutoCancel(true)
- .setExtras(extras)
- .setFullScreenIntent(pendingIntent, true);
-
- // build notification large icon
- Resources res = context.getResources();
- int largeIconResId = res.getIdentifier("ic_launcher", "mipmap", context.getPackageName());
- Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- if (largeIconResId != 0) {
- notificationBuilder.setLargeIcon(largeIconBitmap);
- }
- }
-
- // Reject action
- Intent rejectIntent = new Intent(ACTION_REJECT_CALL)
- .putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId)
- .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- PendingIntent pendingRejectIntent = PendingIntent.getBroadcast(context, 1, rejectIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- notificationBuilder.addAction(0, "DISMISS", pendingRejectIntent);
-
- // Answer action
- Intent answerIntent = new Intent(ACTION_ANSWER_CALL);
- answerIntent
- .putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId)
- .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- PendingIntent pendingAnswerIntent = PendingIntent.getBroadcast(context, 0, answerIntent,
- PendingIntent.FLAG_UPDATE_CURRENT);
- notificationBuilder.addAction(R.drawable.ic_call_white_24dp, "ANSWER", pendingAnswerIntent);
-
- notificationManager.notify(notificationId, notificationBuilder.build());
- TwilioVoiceModule.callNotificationMap.put(INCOMING_NOTIFICATION_PREFIX+callInvite.getCallSid(), notificationId);
- }
-
- public void initCallNotificationsChannel(NotificationManager notificationManager) {
- if (Build.VERSION.SDK_INT < 26) {
- return;
- }
- NotificationChannel channel = new NotificationChannel(VOICE_CHANNEL,
- "Primary Voice Channel", NotificationManager.IMPORTANCE_DEFAULT);
- channel.setLightColor(Color.GREEN);
- channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
- notificationManager.createNotificationChannel(channel);
- }
-
- public void createMissedCallNotification(ReactApplicationContext context, CallInvite callInvite) {
- SharedPreferences sharedPref = context.getSharedPreferences(PREFERENCE_KEY, Context.MODE_PRIVATE);
+ SharedPreferences sharedPref = context.getSharedPreferences(Constants.PREFERENCE_KEY, Context.MODE_PRIVATE);
SharedPreferences.Editor sharedPrefEditor = sharedPref.edit();
- /*
- * Create a PendingIntent to specify the action when the notification is
- * selected in the notification drawer
- */
Intent intent = new Intent(context, getMainActivityClass(context));
- intent.setAction(ACTION_MISSED_CALL)
- .putExtra(INCOMING_CALL_NOTIFICATION_ID, MISSED_CALLS_NOTIFICATION_ID)
+ intent.setAction(Constants.ACTION_MISSED_CALL)
+ .putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, Constants.MISSED_CALLS_NOTIFICATION_ID)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
- Intent clearMissedCallsCountIntent = new Intent(ACTION_CLEAR_MISSED_CALLS_COUNT)
- .putExtra(INCOMING_CALL_NOTIFICATION_ID, CLEAR_MISSED_CALLS_NOTIFICATION_ID);
- PendingIntent clearMissedCallsCountPendingIntent = PendingIntent.getBroadcast(context, 0, clearMissedCallsCountIntent, 0);
+ PendingIntent pendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ intent,
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
+ );
+
+ PendingIntent clearMissedCallsCountPendingIntent = PendingIntent.getBroadcast(
+ context,
+ 0,
+ new Intent(Constants.ACTION_CLEAR_MISSED_CALLS_COUNT)
+ .putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, Constants.CLEAR_MISSED_CALLS_NOTIFICATION_ID),
+ 0
+ );
/*
* Pass the notification id and call sid to use as an identifier to open the notification
*/
Bundle extras = new Bundle();
- extras.putInt(INCOMING_CALL_NOTIFICATION_ID, MISSED_CALLS_NOTIFICATION_ID);
- extras.putString(CALL_SID_KEY, callInvite.getCallSid());
- extras.putString(NOTIFICATION_TYPE, ACTION_MISSED_CALL);
+ extras.putInt(Constants.INCOMING_CALL_NOTIFICATION_ID, Constants.MISSED_CALLS_NOTIFICATION_ID);
+ extras.putString(Constants.CALL_SID_KEY, callSid);
/*
* Create the notification shown in the notification drawer
*/
+ String title = context.getString(R.string.call_missed_title);
NotificationCompat.Builder notification =
new NotificationCompat.Builder(context, VOICE_CHANNEL)
- .setGroup(MISSED_CALLS_GROUP)
+ .setGroup(Constants.MISSED_CALLS_GROUP)
.setGroupSummary(true)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setSmallIcon(R.drawable.ic_call_missed_white_24dp)
- .setContentTitle("Missed call")
- .setContentText(callInvite.getFrom() + " called")
+ .setContentTitle(title)
+ .setContentText(callFrom + context.getString(R.string.call_missed_from))
.setAutoCancel(true)
.setShowWhen(true)
.setExtras(extras)
.setDeleteIntent(clearMissedCallsCountPendingIntent)
.setContentIntent(pendingIntent);
- int missedCalls = sharedPref.getInt(MISSED_CALLS_GROUP, 0);
+ int missedCalls = sharedPref.getInt(Constants.MISSED_CALLS_GROUP, 0);
missedCalls++;
if (missedCalls == 1) {
inboxStyle = new NotificationCompat.InboxStyle();
- inboxStyle.setBigContentTitle("Missed call");
+ inboxStyle.setBigContentTitle(title);
} else {
- inboxStyle.setBigContentTitle(String.valueOf(missedCalls) + " missed calls");
+ inboxStyle.setBigContentTitle(String.valueOf(missedCalls) + " " + context.getString(R.string.call_missed_title_plural));
}
- inboxStyle.addLine("from: " +callInvite.getFrom());
- sharedPrefEditor.putInt(MISSED_CALLS_GROUP, missedCalls);
+ inboxStyle.addLine(context.getString(R.string.call_missed_more) + " " + callFrom);
+ sharedPrefEditor.putInt(Constants.MISSED_CALLS_GROUP, missedCalls);
sharedPrefEditor.commit();
notification.setStyle(inboxStyle);
@@ -262,96 +142,82 @@ public void createMissedCallNotification(ReactApplicationContext context, CallIn
}
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.notify(MISSED_CALLS_NOTIFICATION_ID, notification.build());
+ notificationManager.notify(Constants.MISSED_CALLS_NOTIFICATION_ID, notification.build());
}
- public void createHangupLocalNotification(ReactApplicationContext context, String callSid, String caller) {
- PendingIntent pendingHangupIntent = PendingIntent.getBroadcast(
+ public void createHangupNotification(ReactApplicationContext context, String callSid, String caller) {
+ Intent intent = new Intent(context, getMainActivityClass(context));
+ intent.setAction(Constants.ACTION_OPEN_CALL_IN_PROGRESS)
+ .putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, Constants.HANGUP_NOTIFICATION_ID)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+
+ PendingIntent pendingIntent = PendingIntent.getActivity(
context,
0,
- new Intent(ACTION_HANGUP_CALL).putExtra(INCOMING_CALL_NOTIFICATION_ID, HANGUP_NOTIFICATION_ID),
- PendingIntent.FLAG_UPDATE_CURRENT
+ intent,
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
);
- Intent launchIntent = new Intent(context, getMainActivityClass(context));
- launchIntent.setAction(ACTION_INCOMING_CALL)
- .putExtra(INCOMING_CALL_NOTIFICATION_ID, HANGUP_NOTIFICATION_ID)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- PendingIntent activityPendingIntent = PendingIntent.getActivity(context, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+ PendingIntent hangupPendingIntent = PendingIntent.getBroadcast(
+ context,
+ 0,
+ new Intent(Constants.ACTION_HANGUP_CALL)
+ .putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, Constants.HANGUP_NOTIFICATION_ID),
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
+ );
- /*
- * Pass the notification id and call sid to use as an identifier to cancel the
- * notification later
- */
Bundle extras = new Bundle();
- extras.putInt(INCOMING_CALL_NOTIFICATION_ID, HANGUP_NOTIFICATION_ID);
- extras.putString(CALL_SID_KEY, callSid);
- extras.putString(NOTIFICATION_TYPE, ACTION_HANGUP_CALL);
+ extras.putInt(Constants.INCOMING_CALL_NOTIFICATION_ID, Constants.HANGUP_NOTIFICATION_ID);
+ extras.putString(Constants.CALL_SID_KEY, callSid);
- NotificationCompat.Builder notification = new NotificationCompat.Builder(context, VOICE_CHANNEL)
- .setContentTitle("Call in progress")
- .setContentText(caller)
- .setSmallIcon(R.drawable.ic_call_white_24dp)
- .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
- .setPriority(NotificationCompat.PRIORITY_HIGH)
- .setCategory(NotificationCompat.CATEGORY_CALL)
- .setOngoing(true)
- .setUsesChronometer(true)
- .setExtras(extras)
- .setContentIntent(activityPendingIntent);
-
- notification.addAction(0, "HANG UP", pendingHangupIntent);
+ Notification notification;
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- // Create notifications channel (required for API > 25)
- initCallNotificationsChannel(notificationManager);
- notificationManager.notify(HANGUP_NOTIFICATION_ID, notification.build());
- }
-
- public void removeIncomingCallNotification(ReactApplicationContext context,
- CancelledCallInvite callInvite,
- int notificationId) {
- 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) {
- /*
- * 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.
- */
- StatusBarNotification[] activeNotifications = notificationManager.getActiveNotifications();
- for (StatusBarNotification statusBarNotification : activeNotifications) {
- Notification notification = statusBarNotification.getNotification();
- String notificationType = notification.extras.getString(NOTIFICATION_TYPE);
- if (callInvite.getCallSid().equals(notification.extras.getString(CALL_SID_KEY)) &&
- notificationType != null && notificationType.equals(ACTION_INCOMING_CALL)) {
- notificationManager.cancel(notification.extras.getInt(INCOMING_CALL_NOTIFICATION_ID));
- }
- }
- } else if (notificationId != 0) {
- notificationManager.cancel(notificationId);
- }
+ String title = context.getString(R.string.call_in_progress);
+ String actionText = context.getString(R.string.hangup);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ notification = new NotificationCompat.Builder(context, createChannel(title, notificationManager))
+ .setContentTitle(title)
+ .setContentText(caller)
+ .setSmallIcon(R.drawable.ic_call_white_24dp)
+ .setCategory(Notification.CATEGORY_CALL)
+ .setExtras(extras)
+ .setOngoing(true)
+ .setUsesChronometer(true)
+ .setFullScreenIntent(pendingIntent, true)
+ .addAction(0, actionText, hangupPendingIntent)
+ .build();
} else {
- if (notificationId != 0) {
- notificationManager.cancel(notificationId);
- } else if (callInvite != null) {
- String notificationKey = INCOMING_NOTIFICATION_PREFIX+callInvite.getCallSid();
- if (TwilioVoiceModule.callNotificationMap.containsKey(notificationKey)) {
- notificationId = TwilioVoiceModule.callNotificationMap.get(notificationKey);
- notificationManager.cancel(notificationId);
- TwilioVoiceModule.callNotificationMap.remove(notificationKey);
- }
- }
+ // noinspection deprecation
+ notification = new NotificationCompat.Builder(context)
+ .setContentTitle(title)
+ .setContentText(caller)
+ .setSmallIcon(R.drawable.ic_call_white_24dp)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_CALL)
+ .setOngoing(true)
+ .setUsesChronometer(true)
+ .setExtras(extras)
+ .setContentIntent(pendingIntent)
+ .addAction(0, actionText, hangupPendingIntent)
+ .build();
}
+ notificationManager.notify(Constants.HANGUP_NOTIFICATION_ID, notification);
+ }
+
+ @TargetApi(Build.VERSION_CODES.O)
+ private String createChannel(String channelName, NotificationManager notificationManager) {
+ String channelId = VOICE_CHANNEL;
+ NotificationChannel channel = new NotificationChannel(channelId,
+ channelName, NotificationManager.IMPORTANCE_DEFAULT);
+ channel.setLightColor(Color.GREEN);
+ channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
+ notificationManager.createNotificationChannel(channel);
+ return channelId;
}
public void removeHangupNotification(ReactApplicationContext context) {
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
- notificationManager.cancel(HANGUP_NOTIFICATION_ID);
+ notificationManager.cancel(Constants.HANGUP_NOTIFICATION_ID);
}
}
diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/Constants.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/Constants.java
new file mode 100644
index 00000000..f90b7413
--- /dev/null
+++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/Constants.java
@@ -0,0 +1,45 @@
+package com.hoxfon.react.RNTwilioVoice;
+
+import com.twilio.voice.Call;
+
+public class Constants {
+ 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.RNTwilioVoice.PREFERENCE_FILE_KEY";
+
+ public static final String CALL_SID_KEY = "CALL_SID";
+ public static final String VOICE_CHANNEL_LOW_IMPORTANCE = "notification-channel-low-importance";
+ public static final String VOICE_CHANNEL_HIGH_IMPORTANCE = "notification-channel-high-importance";
+ public static final String INCOMING_CALL_INVITE = "INCOMING_CALL_INVITE";
+ public static final String CANCELLED_CALL_INVITE = "CANCELLED_CALL_INVITE";
+ public static final String CANCELLED_CALL_INVITE_EXCEPTION = "CANCELLED_CALL_INVITE_ERROR";
+ public static final String INCOMING_CALL_NOTIFICATION_ID = "INCOMING_CALL_NOTIFICATION_ID";
+ public static final String ACTION_ACCEPT = "com.hoxfon.react.RNTwilioVoice.ACTION_ACCEPT";
+ public static final String ACTION_REJECT = "com.hoxfon.react.RNTwilioVoice.ACTION_REJECT";
+ public static final String ACTION_MISSED_CALL = "MISSED_CALL";
+ public static final String ACTION_HANGUP_CALL = "HANGUP_CALL";
+ public static final String ACTION_INCOMING_CALL = "ACTION_INCOMING_CALL";
+ public static final String ACTION_INCOMING_CALL_NOTIFICATION = "ACTION_INCOMING_CALL_NOTIFICATION";
+ public static final String ACTION_CANCEL_CALL = "ACTION_CANCEL_CALL";
+ public static final String ACTION_FCM_TOKEN = "ACTION_FCM_TOKEN";
+ public static final String ACTION_CLEAR_MISSED_CALLS_COUNT = "CLEAR_MISSED_CALLS_COUNT";
+ public static final String ACTION_OPEN_CALL_IN_PROGRESS = "CALL_IN_PROGRESS";
+ public static final String ACTION_JS_ANSWER = "ACTION_JS_ANSWER";
+ public static final String ACTION_JS_REJECT = "ACTION_JS_REJECT";
+
+ public static final String CALL_SID = "call_sid";
+ public static final String CALL_STATE = "call_state";
+ public static final String CALL_FROM = "call_from";
+ public static final String CALL_TO = "call_to";
+ public static final String ERROR = "err";
+ public static final String CALL_KEY = "call";
+ public static final String CALL_INVITE_KEY = "callInvite";
+ public static final String CALL_STATE_CONNECTED = Call.State.CONNECTED.toString();
+ public static final String SELECTED_AUDIO_DEVICE = "selected_audio_device";
+ public static final String CALLER_VERIFICATION_STATUS = "caller_verification";
+ public static final String CALLER_VERIFICATION_VERIFIED = "verified";
+ public static final String CALLER_VERIFICATION_UNVERIFIED = "unverified";
+ public static final String CALLER_VERIFICATION_UNKNOWN = "unknown";
+}
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 753c2b32..a843205e 100644
--- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java
+++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/EventManager.java
@@ -25,7 +25,7 @@ public class EventManager {
public static final String EVENT_CALL_INVITE_CANCELLED = "callInviteCancelled";
public static final String EVENT_CONNECTION_IS_RECONNECTING = "connectionIsReconnecting";
public static final String EVENT_CONNECTION_DID_RECONNECT = "connectionDidReconnect";
-
+ public static final String EVENT_AUDIO_DEVICES_UPDATED = "audioDevicesUpdated";
public EventManager(ReactApplicationContext context) {
mContext = context;
diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java
new file mode 100644
index 00000000..c78175c0
--- /dev/null
+++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java
@@ -0,0 +1,330 @@
+package com.hoxfon.react.RNTwilioVoice;
+
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+
+import android.os.Build;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.style.ForegroundColorSpan;
+import android.util.Log;
+
+import androidx.annotation.ColorRes;
+import androidx.annotation.StringRes;
+import androidx.core.app.NotificationCompat;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.ProcessLifecycleOwner;
+import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+
+import com.twilio.voice.CallInvite;
+
+import static com.hoxfon.react.RNTwilioVoice.CallNotificationManager.getMainActivityClass;
+import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG;
+
+public class IncomingCallNotificationService extends Service {
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "IncomingCallNotificationService onStartCommand() intent: " + intent + ", flags: " + flags);
+ }
+ String action = intent.getAction();
+
+ CallInvite callInvite = intent.getParcelableExtra(Constants.INCOMING_CALL_INVITE);
+ int notificationId = intent.getIntExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, 0);
+
+ switch (action) {
+ // when a callInvite is received in the background
+ case Constants.ACTION_INCOMING_CALL:
+ handleIncomingCall(callInvite, notificationId);
+ break;
+
+ case Constants.ACTION_ACCEPT:
+ accept(callInvite, notificationId);
+ break;
+
+ case Constants.ACTION_REJECT:
+ reject(callInvite, notificationId);
+ break;
+
+ case Constants.ACTION_CANCEL_CALL:
+ handleCancelledCall(intent);
+ break;
+
+ case Constants.ACTION_JS_ANSWER:
+ endForeground();
+ break;
+
+ case Constants.ACTION_JS_REJECT:
+ endForeground();
+ break;
+
+ default:
+ break;
+ }
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ private Notification createNotification(CallInvite callInvite, int notificationId, int channelImportance) {
+ Context context = getApplicationContext();
+
+ Intent intent = new Intent(this, getMainActivityClass(context));
+ intent.setAction(Constants.ACTION_INCOMING_CALL_NOTIFICATION);
+ intent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);
+ intent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite);
+ intent.putExtra(Constants.CALL_SID, callInvite.getCallSid());
+ intent.putExtra(Constants.CALL_FROM, callInvite.getFrom());
+ intent.putExtra(Constants.CALL_TO, callInvite.getTo());
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ PendingIntent pendingIntent =
+ PendingIntent.getActivity(this, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ /*
+ * Pass the notification id and call sid to use as an identifier to cancel the
+ * notification later
+ */
+ Bundle extras = new Bundle();
+ extras.putString(Constants.CALL_SID_KEY, callInvite.getCallSid());
+
+ String contentText = callInvite.getFrom() + " " + getString(R.string.call_incoming_content);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ return buildNotification(contentText,
+ pendingIntent,
+ extras,
+ callInvite,
+ notificationId,
+ createChannel(channelImportance));
+ } else {
+ // noinspection deprecation
+ return new NotificationCompat.Builder(this)
+ .setSmallIcon(R.drawable.ic_call_white_24dp)
+ .setContentTitle(getString(R.string.call_incoming_title))
+ .setContentText(contentText)
+ .setAutoCancel(true)
+ .setExtras(extras)
+ .setContentIntent(pendingIntent)
+ .setGroup("test_app_notification")
+ .setColor(Color.rgb(214, 10, 37))
+ .build();
+ }
+ }
+
+ private Spannable getActionText(Context context, @StringRes int stringRes, @ColorRes int colorRes) {
+ Spannable spannable = new SpannableString(context.getText(stringRes));
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
+ spannable.setSpan(
+ new ForegroundColorSpan(context.getColor(colorRes)),
+ 0,
+ spannable.length(),
+ 0
+ );
+ }
+ return spannable;
+ }
+
+ private PendingIntent createActionPendingIntent(Context context, Intent intent) {
+ return PendingIntent.getService(
+ context,
+ 0,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT
+ );
+ }
+
+ /**
+ * Build a notification.
+ *
+ * @param text the text of the notification
+ * @param pendingIntent the body, pending intent for the notification
+ * @param extras extras passed with the notification
+ * @return the builder
+ */
+ @TargetApi(Build.VERSION_CODES.O)
+ private Notification buildNotification(String text,
+ PendingIntent pendingIntent,
+ Bundle extras,
+ final CallInvite callInvite,
+ int notificationId,
+ String channelId) {
+ Context context = getApplicationContext();
+
+ Intent rejectIntent = new Intent(context, IncomingCallNotificationService.class);
+ rejectIntent.setAction(Constants.ACTION_REJECT);
+ rejectIntent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite);
+ rejectIntent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);
+ NotificationCompat.Action rejectAction = new NotificationCompat.Action.Builder(
+ android.R.drawable.ic_menu_delete,
+ getActionText(context, R.string.reject, R.color.red),
+ createActionPendingIntent(context, rejectIntent)
+ ).build();
+
+ Intent acceptIntent = new Intent(context, IncomingCallNotificationService.class);
+ acceptIntent.setAction(Constants.ACTION_ACCEPT);
+ acceptIntent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite);
+ acceptIntent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);
+ NotificationCompat.Action answerAction = new NotificationCompat.Action.Builder(
+ android.R.drawable.ic_menu_call,
+ getActionText(context, R.string.accept, R.color.green),
+ createActionPendingIntent(context, acceptIntent)
+ ).build();
+
+ NotificationCompat.Builder builder =
+ new NotificationCompat.Builder(context, channelId)
+ .setSmallIcon(R.drawable.ic_call_white_24dp)
+ .setContentTitle(getString(R.string.call_incoming_title))
+ .setContentText(text)
+ .setExtras(extras)
+ .setAutoCancel(true)
+ .addAction(rejectAction)
+ .addAction(answerAction)
+ .setFullScreenIntent(pendingIntent, true)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(Notification.CATEGORY_CALL)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ ;
+
+ // build notification large icon
+ Resources res = context.getResources();
+ int largeIconResId = res.getIdentifier("ic_launcher", "mipmap", context.getPackageName());
+ Bitmap largeIconBitmap = BitmapFactory.decodeResource(res, largeIconResId);
+
+ if (largeIconResId != 0) {
+ builder.setLargeIcon(largeIconBitmap);
+ }
+
+ return builder.build();
+ }
+
+ @TargetApi(Build.VERSION_CODES.O)
+ private String createChannel(int channelImportance) {
+ String channelId = Constants.VOICE_CHANNEL_HIGH_IMPORTANCE;
+ if (channelImportance == NotificationManager.IMPORTANCE_LOW) {
+ channelId = Constants.VOICE_CHANNEL_LOW_IMPORTANCE;
+ }
+ NotificationChannel callInviteChannel = new NotificationChannel(channelId,
+ "Incoming calls", channelImportance);
+ callInviteChannel.setLightColor(Color.GREEN);
+
+ // TODO set sound for background incoming call
+// Uri defaultRingtoneUri = RingtoneManager.getActualDefaultRingtoneUri(this, RingtoneManager.TYPE_RINGTONE);
+// AudioAttributes audioAttributes = new AudioAttributes.Builder()
+// .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+// .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+// .build();
+// callInviteChannel.setSound(defaultRingtoneUri, audioAttributes);
+
+ callInviteChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
+ NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ notificationManager.createNotificationChannel(callInviteChannel);
+
+ return channelId;
+ }
+
+ private void accept(CallInvite callInvite, int notificationId) {
+ endForeground();
+ Intent activeCallIntent = new Intent(this, getMainActivityClass(this));
+ activeCallIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ activeCallIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ activeCallIntent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite);
+ activeCallIntent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);
+ activeCallIntent.putExtra(Constants.CALL_SID, callInvite.getCallSid());
+ activeCallIntent.putExtra(Constants.CALL_FROM, callInvite.getFrom());
+ activeCallIntent.putExtra(Constants.CALL_TO, callInvite.getTo());
+ activeCallIntent.setAction(Constants.ACTION_ACCEPT);
+ this.startActivity(activeCallIntent);
+ }
+
+ private void reject(CallInvite callInvite, int notificationId) {
+ SoundPoolManager.getInstance(this).stopRinging();
+ endForeground();
+ callInvite.reject(getApplicationContext());
+ }
+
+ private void handleCancelledCall(Intent intent) {
+ SoundPoolManager.getInstance(this).stopRinging();
+ endForeground();
+ LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+ }
+
+ private void handleIncomingCall(CallInvite callInvite, int notificationId) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ setCallInProgressNotification(callInvite, notificationId);
+ }
+ sendCallInviteToActivity(callInvite, notificationId);
+ }
+
+ private void endForeground() {
+ stopForeground(true);
+ }
+
+ @TargetApi(Build.VERSION_CODES.O)
+ private void setCallInProgressNotification(CallInvite callInvite, int notificationId) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "setCallInProgressNotification()");
+ }
+ int importance = NotificationManager.IMPORTANCE_LOW;
+ if (!isAppVisible()) {
+ if (BuildConfig.DEBUG) {
+ Log.i(TAG, "app is NOT visible.");
+ }
+ importance = NotificationManager.IMPORTANCE_HIGH;
+ }
+ this.startForeground(notificationId, createNotification(callInvite, notificationId, importance));
+ }
+
+ /*
+ * Send the CallInvite to the Activity. Start the activity if it is not running already.
+ */
+ private void sendCallInviteToActivity(CallInvite callInvite, int notificationId) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "sendCallInviteToActivity(). Android SDK: " + Build.VERSION.SDK_INT + " app visible: " + isAppVisible());
+ }
+ SoundPoolManager.getInstance(this).playRinging();
+
+ // From Android SDK 29 apps are prevented to start an activity from the background
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !isAppVisible()) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "sendCallInviteToActivity(). DO NOTHING");
+ }
+ return;
+ }
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "sendCallInviteToActivity(). startActivity()");
+ }
+ // Android SDK < 29 or app is visible
+ Intent intent = new Intent(this, getMainActivityClass(this));
+ intent.setAction(Constants.ACTION_INCOMING_CALL);
+ intent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);
+ intent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite);
+ intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ this.startActivity(intent);
+ }
+
+ private boolean isAppVisible() {
+ return ProcessLifecycleOwner
+ .get()
+ .getLifecycle()
+ .getCurrentState()
+ .isAtLeast(Lifecycle.State.STARTED);
+ }
+}
\ No newline at end of file
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..b79852db 100644
--- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/SoundPoolManager.java
+++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/SoundPoolManager.java
@@ -1,6 +1,7 @@
package com.hoxfon.react.RNTwilioVoice;
import android.content.Context;
+import android.media.AudioAttributes;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
@@ -14,6 +15,11 @@ public class SoundPoolManager {
private SoundPoolManager(Context context) {
Uri ringtoneSound = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
ringtone = RingtoneManager.getRingtone(context, ringtoneSound);
+ AudioAttributes alarmAttribute = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_ALARM)
+ .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
+ .build();
+ ringtone.setAudioAttributes(alarmAttribute);
}
public static SoundPoolManager getInstance(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 d9c627fb..7874a1ca 100644
--- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java
+++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java
@@ -3,25 +3,22 @@
import android.Manifest;
import android.app.Activity;
import android.app.ActivityManager.RunningAppProcessInfo;
-import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
-import android.media.AudioAttributes;
-import android.media.AudioFocusRequest;
import android.media.AudioManager;
-import android.os.Build;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
+import kotlin.Unit;
+
+import android.os.Bundle;
import android.util.Log;
-import android.view.Window;
-import android.view.WindowManager;
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
import com.facebook.react.bridge.AssertionException;
@@ -39,10 +36,10 @@
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;
-import com.google.firebase.iid.InstanceIdResult;
+
+import com.twilio.audioswitch.AudioDevice;
+import com.twilio.audioswitch.AudioSwitch;
import com.twilio.voice.AcceptOptions;
import com.twilio.voice.Call;
import com.twilio.voice.CallException;
@@ -52,10 +49,15 @@
import com.twilio.voice.LogLevel;
import com.twilio.voice.RegistrationException;
import com.twilio.voice.RegistrationListener;
+import com.twilio.voice.UnregistrationListener;
import com.twilio.voice.Voice;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
import java.util.HashMap;
import java.util.Map;
+import java.util.List;
import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CONNECTION_DID_CONNECT;
import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CONNECTION_DID_DISCONNECT;
@@ -66,6 +68,7 @@
import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CALL_INVITE_CANCELLED;
import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CONNECTION_IS_RECONNECTING;
import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CONNECTION_DID_RECONNECT;
+import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_AUDIO_DEVICES_UPDATED;
public class TwilioVoiceModule extends ReactContextBaseJavaModule implements ActivityEventListener, LifecycleEventListener {
@@ -73,40 +76,12 @@ public class TwilioVoiceModule extends ReactContextBaseJavaModule implements Act
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_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";
- 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_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";
- 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 NotificationManager notificationManager;
private CallNotificationManager callNotificationManager;
private ProximityManager proximityManager;
@@ -118,21 +93,28 @@ public class TwilioVoiceModule extends ReactContextBaseJavaModule implements Act
static Map callNotificationMap;
private RegistrationListener registrationListener = registrationListener();
+ private UnregistrationListener unregistrationListener = unregistrationListener();
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 int existingCallInviteIntent;
+
+ /*
+ * Audio device management
+ */
+ private AudioSwitch audioSwitch;
+ private int savedVolumeControlStream;
+ AudioDevice selectedAudioDevice;
+ Map availableAudioDevices;
public TwilioVoiceModule(ReactApplicationContext reactContext,
- boolean shouldAskForMicPermission) {
+ boolean shouldAskForMicPermission) {
super(reactContext);
+
if (BuildConfig.DEBUG) {
Voice.setLogLevel(LogLevel.DEBUG);
} else {
@@ -146,8 +128,6 @@ public TwilioVoiceModule(ReactApplicationContext reactContext,
proximityManager = new ProximityManager(reactContext, eventManager);
headsetManager = new HeadsetManager(eventManager);
- notificationManager = (android.app.NotificationManager) reactContext.getSystemService(Context.NOTIFICATION_SERVICE);
-
/*
* Setup the broadcast receiver to be notified of GCM Token updates
* or incoming call messages in this Activity.
@@ -157,10 +137,8 @@ public TwilioVoiceModule(ReactApplicationContext reactContext,
TwilioVoiceModule.callNotificationMap = new HashMap<>();
- /*
- * Needed for setting/abandoning audio focus during a call
- */
- audioManager = (AudioManager) reactContext.getSystemService(Context.AUDIO_SERVICE);
+ audioSwitch = new AudioSwitch(reactContext);
+ availableAudioDevices = new HashMap<>();
/*
* Ensure the microphone permission is enabled
@@ -172,11 +150,37 @@ public TwilioVoiceModule(ReactApplicationContext reactContext,
@Override
public void onHostResume() {
+ savedVolumeControlStream = getCurrentActivity().getVolumeControlStream();
/*
* Enable changing the volume using the up/down keys during a conversation
*/
getCurrentActivity().setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
registerReceiver();
+
+ Intent intent = getCurrentActivity().getIntent();
+ if (intent == null || intent.getAction() == null) {
+ return;
+ }
+ int currentCallInviteIntent = intent.hashCode();
+ String action = intent.getAction();
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "onHostResume(). Action: " + action + ". Intent: " + intent.getExtras());
+ }
+
+ if (action.equals(Intent.ACTION_MAIN)) {
+ return;
+ }
+
+ if (action.equals(Constants.ACTION_ACCEPT) && currentCallInviteIntent == existingCallInviteIntent) {
+ return;
+ }
+
+ if (action.equals(Constants.ACTION_INCOMING_CALL_NOTIFICATION) && currentCallInviteIntent == existingCallInviteIntent) {
+ return;
+ }
+
+ existingCallInviteIntent = currentCallInviteIntent;
+ handleStartActivityIntent(intent);
}
@Override
@@ -189,7 +193,11 @@ public void onHostPause() {
public void onHostDestroy() {
disconnect();
callNotificationManager.removeHangupNotification(getReactApplicationContext());
- unsetAudioFocus();
+ /*
+ * Tear down audio device management and restore previous volume stream
+ */
+ audioSwitch.stop();
+ getCurrentActivity().setVolumeControlStream(savedVolumeControlStream);
}
@Override
@@ -197,34 +205,53 @@ public String getName() {
return TAG;
}
+ @Override
public void onNewIntent(Intent intent) {
// This is called only when the App is in the foreground
if (BuildConfig.DEBUG) {
- Log.d(TAG, "onNewIntent " + intent.toString());
+ Log.d(TAG, "onNewIntent(). Intent: " + intent.toString());
}
- handleIncomingCallIntent(intent);
+ handleStartActivityIntent(intent);
}
private RegistrationListener registrationListener() {
return new RegistrationListener() {
@Override
- public void onRegistered(String accessToken, String fcmToken) {
+ public void onRegistered(@NonNull String accessToken, @NonNull String fcmToken) {
if (BuildConfig.DEBUG) {
- Log.d(TAG, "Successfully registered FCM");
+ Log.d(TAG, "RegistrationListener().onRegistered(). FCM registered.");
}
eventManager.sendEvent(EVENT_DEVICE_READY, null);
}
@Override
- public void onError(RegistrationException error, String accessToken, String fcmToken) {
- Log.e(TAG, String.format("Registration Error: %d, %s", error.getErrorCode(), error.getMessage()));
+ public void onError(@NonNull RegistrationException error,
+ @NonNull String accessToken,
+ @NonNull String fcmToken) {
+ Log.e(TAG, String.format("RegistrationListener().onError(). Code: %d. %s", error.getErrorCode(), error.getMessage()));
WritableMap params = Arguments.createMap();
- params.putString("err", error.getMessage());
+ params.putString(Constants.ERROR, error.getMessage());
eventManager.sendEvent(EVENT_DEVICE_NOT_READY, params);
}
};
}
+ private UnregistrationListener unregistrationListener() {
+ return new UnregistrationListener() {
+ @Override
+ public void onUnregistered(String accessToken, String fcmToken) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "Successfully unregistered FCM");
+ }
+ }
+
+ @Override
+ public void onError(RegistrationException error, String accessToken, String fcmToken) {
+ Log.e(TAG, String.format("Unregistration Error: %d, %s", error.getErrorCode(), error.getMessage()));
+ }
+ };
+ }
+
private Call.Listener callListener() {
return new Call.Listener() {
/*
@@ -241,46 +268,44 @@ private Call.Listener callListener() {
* raised, irrespective of the value of answerOnBridge being set to true or false
*/
@Override
- public void onRinging(Call call) {
+ public void onRinging(@NonNull 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());
+ Log.d(TAG, "Call.Listener().onRinging(). Call state: " + call.getState() + ". Call: "+ call.toString());
}
WritableMap params = Arguments.createMap();
if (call != null) {
- params.putString("call_sid", call.getSid());
- params.putString("call_from", call.getFrom());
+ params.putString(Constants.CALL_SID, call.getSid());
+ params.putString(Constants.CALL_FROM, call.getFrom());
}
eventManager.sendEvent(EVENT_CALL_STATE_RINGING, params);
}
@Override
- public void onConnected(Call call) {
+ public void onConnected(@NonNull Call call) {
if (BuildConfig.DEBUG) {
- Log.d(TAG, "CALL CONNECTED callListener().onConnected call state = "+call.getState());
+ Log.d(TAG, "Call.Listener().onConnected(). Call state: " + call.getState());
}
- setAudioFocus();
+ audioSwitch.activate();
proximityManager.startProximitySensor();
headsetManager.startWiredHeadsetEvent(getReactApplicationContext());
WritableMap params = Arguments.createMap();
- if (call != null) {
- 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());
- String caller = "Show call details in the app";
- if (!toName.equals("")) {
- caller = toName;
- } else if (!toNumber.equals("")) {
- caller = toNumber;
- }
- activeCall = call;
- callNotificationManager.createHangupLocalNotification(getReactApplicationContext(),
- call.getSid(), caller);
+ params.putString(Constants.CALL_SID, call.getSid());
+ params.putString(Constants.CALL_STATE, call.getState().name());
+ params.putString(Constants.CALL_FROM, call.getFrom());
+ params.putString(Constants.CALL_TO, call.getTo());
+ String caller = "Show call details in the app";
+ if (!toName.equals("")) {
+ caller = toName;
+ } else if (!toNumber.equals("")) {
+ caller = toNumber;
}
+ activeCall = call;
+ callNotificationManager.createHangupNotification(getReactApplicationContext(),
+ call.getSid(), caller);
eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params);
+ activeCallInvite = null;
}
/**
@@ -291,16 +316,13 @@ public void onConnected(Call call) {
@Override
public void onReconnecting(@NonNull Call call, @NonNull CallException callException) {
if (BuildConfig.DEBUG) {
- Log.d(TAG, "CALL RECONNECTING callListener().onReconnecting call state = "+call.getState());
+ Log.d(TAG, "Call.Listener().onReconnecting(). Call state: " + call.getState());
}
WritableMap params = Arguments.createMap();
- if (call != null) {
- params.putString("call_sid", call.getSid());
- params.putString("call_from", call.getFrom());
- params.putString("call_to", call.getTo());
- }
+ params.putString(Constants.CALL_SID, call.getSid());
+ params.putString(Constants.CALL_FROM, call.getFrom());
+ params.putString(Constants.CALL_TO, call.getTo());
eventManager.sendEvent(EVENT_CONNECTION_IS_RECONNECTING, params);
-
}
/**
@@ -309,40 +331,35 @@ public void onReconnecting(@NonNull Call call, @NonNull CallException callExcept
@Override
public void onReconnected(@NonNull Call call) {
if (BuildConfig.DEBUG) {
- Log.d(TAG, "CALL RECONNECTED callListener().onReconnected call state = "+call.getState());
+ Log.d(TAG, "Call.Listener().onReconnected(). Call state: " + call.getState());
}
WritableMap params = Arguments.createMap();
- if (call != null) {
- params.putString("call_sid", call.getSid());
- params.putString("call_from", call.getFrom());
- params.putString("call_to", call.getTo());
- }
+ params.putString(Constants.CALL_SID, call.getSid());
+ params.putString(Constants.CALL_FROM, call.getFrom());
+ params.putString(Constants.CALL_TO, call.getTo());
eventManager.sendEvent(EVENT_CONNECTION_DID_RECONNECT, params);
}
@Override
- public void onDisconnected(Call call, CallException error) {
+ public void onDisconnected(@NonNull Call call, CallException error) {
if (BuildConfig.DEBUG) {
- Log.d(TAG, "CALL DISCONNECTED callListener().onDisconnected call state = "+call.getState());
+ Log.d(TAG, "Call.Listener().onDisconnected(). Call state: " + call.getState());
}
- unsetAudioFocus();
+ audioSwitch.deactivate();
proximityManager.stopProximitySensor();
headsetManager.stopWiredHeadsetEvent(getReactApplicationContext());
- callAccepted = false;
WritableMap params = Arguments.createMap();
String callSid = "";
- if (call != null) {
- callSid = call.getSid();
- params.putString("call_sid", callSid);
- params.putString("call_state", call.getState().name());
- params.putString("call_from", call.getFrom());
- params.putString("call_to", call.getTo());
- }
+ callSid = call.getSid();
+ params.putString(Constants.CALL_SID, callSid);
+ params.putString(Constants.CALL_STATE, call.getState().name());
+ params.putString(Constants.CALL_FROM, call.getFrom());
+ params.putString(Constants.CALL_TO, call.getTo());
if (error != null) {
Log.e(TAG, String.format("CallListener onDisconnected error: %d, %s",
error.getErrorCode(), error.getMessage()));
- params.putString("err", error.getMessage());
+ params.putString(Constants.ERROR, error.getMessage());
}
if (callSid != null && activeCall != null && activeCall.getSid() != null && activeCall.getSid().equals(callSid)) {
activeCall = null;
@@ -351,31 +368,28 @@ public void onDisconnected(Call call, CallException error) {
callNotificationManager.removeHangupNotification(getReactApplicationContext());
toNumber = "";
toName = "";
+ activeCallInvite = null;
}
@Override
- public void onConnectFailure(Call call, CallException error) {
+ public void onConnectFailure(@NonNull Call call, CallException error) {
if (BuildConfig.DEBUG) {
- Log.d(TAG, "CALL FAILURE callListener().onConnectFailure call state = "+call.getState());
+ Log.d(TAG, "Call.Listener().onConnectFailure(). Call state: " + call.getState());
}
- unsetAudioFocus();
+ audioSwitch.deactivate();
proximityManager.stopProximitySensor();
- callAccepted = false;
-
Log.e(TAG, String.format("CallListener onConnectFailure error: %d, %s",
- error.getErrorCode(), error.getMessage()));
+ error.getErrorCode(), error.getMessage()));
WritableMap params = Arguments.createMap();
- params.putString("err", error.getMessage());
+ params.putString(Constants.ERROR, error.getMessage());
String callSid = "";
- if (call != null) {
- callSid = call.getSid();
- params.putString("call_sid", callSid);
- params.putString("call_state", call.getState().name());
- params.putString("call_from", call.getFrom());
- params.putString("call_to", call.getTo());
- }
+ callSid = call.getSid();
+ params.putString(Constants.CALL_SID, callSid);
+ params.putString(Constants.CALL_STATE, call.getState().name());
+ params.putString(Constants.CALL_FROM, call.getFrom());
+ params.putString(Constants.CALL_TO, call.getTo());
if (callSid != null && activeCall != null && activeCall.getSid() != null && activeCall.getSid().equals(callSid)) {
activeCall = null;
}
@@ -383,6 +397,35 @@ public void onConnectFailure(Call call, CallException error) {
callNotificationManager.removeHangupNotification(getReactApplicationContext());
toNumber = "";
toName = "";
+ activeCallInvite = null;
+ }
+
+ /*
+ * currentWarnings: existing quality warnings that have not been cleared yet
+ * previousWarnings: last set of warnings prior to receiving this callback
+ *
+ * Example:
+ * - currentWarnings: { A, B }
+ * - previousWarnings: { B, C }
+ *
+ * Newly raised warnings = currentWarnings - intersection = { A }
+ * Newly cleared warnings = previousWarnings - intersection = { C }
+ */
+ public void onCallQualityWarningsChanged(@NonNull Call call,
+ @NonNull Set currentWarnings,
+ @NonNull Set previousWarnings) {
+ if (previousWarnings.size() > 1) {
+ Set intersection = new HashSet<>(currentWarnings);
+ currentWarnings.removeAll(previousWarnings);
+ intersection.retainAll(previousWarnings);
+ previousWarnings.removeAll(intersection);
+ }
+ String message = String.format(
+ Locale.US,
+ "Newly raised warnings: " + currentWarnings + " Clear warnings " + previousWarnings);
+ Log.e(TAG, message);
+
+ // TODO send event to JS
}
};
}
@@ -391,57 +434,82 @@ public void onConnectFailure(Call call, CallException error) {
* Register the Voice broadcast receiver
*/
private void registerReceiver() {
+ if (isReceiverRegistered) {
+ return;
+ }
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(Constants.ACTION_INCOMING_CALL);
+ intentFilter.addAction(Constants.ACTION_CANCEL_CALL);
+ LocalBroadcastManager.getInstance(getReactApplicationContext()).registerReceiver(
+ voiceBroadcastReceiver, intentFilter);
+ registerActionReceiver();
+ isReceiverRegistered = true;
+ }
+
+ private void unregisterReceiver() {
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);
- registerActionReceiver();
- isReceiverRegistered = true;
+ return;
}
+ LocalBroadcastManager.getInstance(getReactApplicationContext()).unregisterReceiver(voiceBroadcastReceiver);
+ isReceiverRegistered = false;
+ }
+
+ private void removeMissedCalls() {
+ SharedPreferences sharedPref = getReactApplicationContext().getSharedPreferences(
+ Constants.PREFERENCE_KEY, Context.MODE_PRIVATE);
+ SharedPreferences.Editor sharedPrefEditor = sharedPref.edit();
+ sharedPrefEditor.putInt(Constants.MISSED_CALLS_GROUP, 0);
+ sharedPrefEditor.commit();
}
-// private void unregisterReceiver() {
-// if (isReceiverRegistered) {
-// LocalBroadcastManager.getInstance(getReactApplicationContext()).unregisterReceiver(voiceBroadcastReceiver);
-// isReceiverRegistered = false;
-// }
-// }
+ private class VoiceBroadcastReceiver extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent == null || intent.getAction() == null) {
+ return;
+ }
+ String action = intent.getAction();
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "VoiceBroadcastReceiver.onReceive() action: " + action + ". Intent extra: " + intent.getExtras());
+ }
+ activeCallInvite = intent.getParcelableExtra(Constants.INCOMING_CALL_INVITE);
+
+ switch (action) {
+ // when a callInvite is received in the foreground
+ case Constants.ACTION_INCOMING_CALL:
+ handleCallInviteNotification();
+ break;
+
+ case Constants.ACTION_CANCEL_CALL:
+ handleCancelCall(intent);
+ break;
+
+ }
+ }
+ }
private void registerActionReceiver() {
IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(ACTION_ANSWER_CALL);
- intentFilter.addAction(ACTION_REJECT_CALL);
- intentFilter.addAction(ACTION_HANGUP_CALL);
- intentFilter.addAction(ACTION_CLEAR_MISSED_CALLS_COUNT);
+ intentFilter.addAction(Constants.ACTION_HANGUP_CALL);
+ intentFilter.addAction(Constants.ACTION_CLEAR_MISSED_CALLS_COUNT);
getReactApplicationContext().registerReceiver(new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "BroadcastReceiver.onReceive() action: " + action);
+ }
switch (action) {
- case ACTION_ANSWER_CALL:
- accept();
- break;
- case ACTION_REJECT_CALL:
- reject();
- break;
- case ACTION_HANGUP_CALL:
+ case Constants.ACTION_HANGUP_CALL:
disconnect();
break;
- case ACTION_CLEAR_MISSED_CALLS_COUNT:
- SharedPreferences sharedPref = context.getSharedPreferences(PREFERENCE_KEY, Context.MODE_PRIVATE);
- SharedPreferences.Editor sharedPrefEditor = sharedPref.edit();
- sharedPrefEditor.putInt(MISSED_CALLS_GROUP, 0);
- sharedPrefEditor.commit();
+ case Constants.ACTION_CLEAR_MISSED_CALLS_COUNT:
+ removeMissedCalls();
+ break;
}
- // Dismiss the notification when the user tap on the relative notification action
- // eventually the notification will be cleared anyway
- // but in this way there is no UI lag
- notificationManager.cancel(intent.getIntExtra(INCOMING_CALL_NOTIFICATION_ID, 0));
}
}, intentFilter);
}
@@ -456,100 +524,115 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
// Ignored, required to implement ActivityEventListener for RN 0.33
}
- private void handleIncomingCallIntent(Intent intent) {
+ private void handleStartActivityIntent(Intent intent) {
if (intent == null || intent.getAction() == null) {
return;
}
String action = intent.getAction();
- if (action.equals(ACTION_INCOMING_CALL)) {
- if (BuildConfig.DEBUG) {
- Log.d(TAG, "handleIncomingCallIntent");
- }
- activeCallInvite = intent.getParcelableExtra(INCOMING_CALL_INVITE);
- if (activeCallInvite != null) {
- callAccepted = false;
- SoundPoolManager.getInstance(getReactApplicationContext()).playRinging();
-
- if (getReactApplicationContext().getCurrentActivity() != null) {
- Window window = getReactApplicationContext().getCurrentActivity().getWindow();
- window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
- | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
- );
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "handleStartActivityIntent() action: " + action);
+ }
+ activeCallInvite = intent.getParcelableExtra(Constants.INCOMING_CALL_INVITE);
+
+ switch (action) {
+ case Constants.ACTION_INCOMING_CALL_NOTIFICATION:
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "ACTION_INCOMING_CALL_NOTIFICATION handleStartActivityIntent");
}
- // send a JS event ONLY if the app's importance is FOREGROUND or SERVICE
- // at startup the app would try to fetch the activeIncoming calls
- 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()); // TODO check if needed
- eventManager.sendEvent(EVENT_DEVICE_DID_RECEIVE_INCOMING, params);
+ WritableMap params = Arguments.createMap();
+ params.putString(Constants.CALL_SID, activeCallInvite.getCallSid());
+ params.putString(Constants.CALL_FROM, activeCallInvite.getFrom());
+ params.putString(Constants.CALL_TO, activeCallInvite.getTo());
+ eventManager.sendEvent(EVENT_DEVICE_DID_RECEIVE_INCOMING, params);
+ break;
+
+ case Constants.ACTION_MISSED_CALL:
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "ACTION_MISSED_CALL handleStartActivityIntent");
}
- } else {
- // TODO evaluate what more is needed at this point?
- Log.e(TAG, "ACTION_INCOMING_CALL but not active call");
- }
- } else if (action.equals(ACTION_CANCEL_CALL_INVITE)) {
- SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging();
- if (BuildConfig.DEBUG) {
- Log.d(TAG, "activeCallInvite was cancelled by " + activeCallInvite.getFrom());
- }
- if (!callAccepted) {
+ removeMissedCalls();
+ break;
+
+ case Constants.ACTION_CLEAR_MISSED_CALLS_COUNT:
if (BuildConfig.DEBUG) {
- Log.d(TAG, "creating a missed call");
+ Log.d(TAG, "ACTION_CLEAR_MISSED_CALLS_COUNT handleStartActivityIntent");
}
- 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", Call.State.DISCONNECTED.toString());
- eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params);
+ removeMissedCalls();
+ break;
+
+ case Constants.ACTION_FCM_TOKEN:
+ registerForCallInvites();
+ break;
+
+ case Constants.ACTION_ACCEPT:
+ acceptFromIntent(intent);
+ break;
+
+ case Constants.ACTION_OPEN_CALL_IN_PROGRESS:
+ // the notification already brings the activity to the top
+ if (activeCall == null) {
+ callNotificationManager.removeHangupNotification(getReactApplicationContext());
}
- }
- clearIncomingNotification(activeCallInvite.getCallSid());
- } else if (action.equals(ACTION_FCM_TOKEN)) {
- if (BuildConfig.DEBUG) {
- Log.d(TAG, "handleIncomingCallIntent ACTION_FCM_TOKEN");
- }
- registerForCallInvites();
+ break;
+
+ default:
+ Log.e(TAG, "received broadcast unhandled action " + action);
+ break;
}
}
- private class VoiceBroadcastReceiver extends BroadcastReceiver {
+ private void handleCallInviteNotification() {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "handleCallInviteNotification()");
+ }
+ if (activeCallInvite == null) {
+ Log.e(TAG, "NO active call invite");
+ return;
+ }
+ SoundPoolManager.getInstance(getReactApplicationContext()).playRinging();
- @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)) {
- handleIncomingCallIntent(intent);
- } else if (action.equals(ACTION_CANCEL_CALL_INVITE)) {
- CancelledCallInvite cancelledCallInvite = intent.getParcelableExtra(CANCELLED_CALL_INVITE);
- clearIncomingNotification(cancelledCallInvite.getCallSid());
- WritableMap params = Arguments.createMap();
- if (cancelledCallInvite != null) {
- params.putString("call_sid", cancelledCallInvite.getCallSid());
- params.putString("call_from", cancelledCallInvite.getFrom());
- params.putString("call_to", cancelledCallInvite.getTo());
- }
- eventManager.sendEvent(EVENT_CALL_INVITE_CANCELLED, params);
- } 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);
+ WritableMap params = Arguments.createMap();
+ params.putString(Constants.CALL_SID, activeCallInvite.getCallSid());
+ params.putString(Constants.CALL_FROM, activeCallInvite.getFrom());
+ params.putString(Constants.CALL_TO, activeCallInvite.getTo());
+ String verificationStatus = Constants.CALLER_VERIFICATION_UNKNOWN;
+ if (activeCallInvite.getCallerInfo().isVerified() != null) {
+ verificationStatus = activeCallInvite.getCallerInfo().isVerified() == true
+ ? Constants.CALLER_VERIFICATION_VERIFIED
+ : Constants.CALLER_VERIFICATION_UNVERIFIED
+ ;
+ }
+ params.putString(Constants.CALLER_VERIFICATION_STATUS, verificationStatus);
+ eventManager.sendEvent(EVENT_DEVICE_DID_RECEIVE_INCOMING, params);
+ }
+
+ private void handleCancelCall(Intent intent) {
+ CancelledCallInvite cancelledCallInvite = intent.getParcelableExtra(Constants.CANCELLED_CALL_INVITE);
+
+ ReactApplicationContext ctx = getReactApplicationContext();
+ SoundPoolManager.getInstance(ctx).stopRinging();
+ callNotificationManager.createMissedCallNotification(
+ getReactApplicationContext(),
+ cancelledCallInvite.getCallSid(),
+ cancelledCallInvite.getFrom()
+ );
+
+ WritableMap params = Arguments.createMap();
+
+ // TODO check whether the params should be passed anyway
+ int appImportance = callNotificationManager.getApplicationImportance(ctx);
+ if (appImportance <= RunningAppProcessInfo.IMPORTANCE_VISIBLE) {
+ params.putString(Constants.CALL_SID, cancelledCallInvite.getCallSid());
+ params.putString(Constants.CALL_FROM, cancelledCallInvite.getFrom());
+ params.putString(Constants.CALL_TO, cancelledCallInvite.getTo());
+ String cancelledCallInviteErr = intent.getStringExtra(Constants.CANCELLED_CALL_INVITE_EXCEPTION);
+ // pass this to the event even though in v5.0.2 it is always "Call Cancelled"
+ if (cancelledCallInviteErr != null) {
+ params.putString(Constants.ERROR, cancelledCallInviteErr);
}
}
+ // TODO handle custom parameters
+ eventManager.sendEvent(EVENT_CALL_INVITE_CANCELLED, params);
}
@ReactMethod
@@ -566,27 +649,13 @@ public void initWithAccessToken(final String accessToken, Promise promise) {
TwilioVoiceModule.this.accessToken = accessToken;
if (BuildConfig.DEBUG) {
- Log.d(TAG, "initWithAccessToken");
+ Log.d(TAG, "initWithAccessToken()");
}
registerForCallInvites();
WritableMap params = Arguments.createMap();
params.putBoolean("initialized", true);
promise.resolve(params);
- }
-
- private void clearIncomingNotification(String callSid) {
- if (BuildConfig.DEBUG) {
- Log.d(TAG, "clearIncomingNotification() callSid: "+ callSid);
- }
- // remove incoming call notification
- String notificationKey = INCOMING_NOTIFICATION_PREFIX + callSid;
- int notificationId = 0;
- if (TwilioVoiceModule.callNotificationMap.containsKey(notificationKey)) {
- notificationId = TwilioVoiceModule.callNotificationMap.get(notificationKey);
- }
- callNotificationManager.removeIncomingCallNotification(getReactApplicationContext(), null, notificationId);
- TwilioVoiceModule.callNotificationMap.remove(notificationKey);
- activeCallInvite = null;
+ startAudioSwitch();
}
/*
@@ -597,108 +666,127 @@ private void clearIncomingNotification(String callSid) {
*
*/
private void registerForCallInvites() {
- FirebaseInstanceId.getInstance().getInstanceId()
- .addOnCompleteListener(new OnCompleteListener() {
- @Override
- public void onComplete(@NonNull Task task) {
- if (!task.isSuccessful()) {
- Log.w(TAG, "getInstanceId failed", task.getException());
- return;
- }
+ String fcmToken = FirebaseInstanceId.getInstance().getToken();
+ if (fcmToken == null) {
+ return;
+ }
+ if (BuildConfig.DEBUG) {
+ Log.i(TAG, "Registering with FCM");
+ }
+ Voice.register(accessToken, Voice.RegistrationChannel.FCM, fcmToken, registrationListener);
+ }
+
+ /*
+ * Unregister your android device with Twilio
+ *
+ */
- // Get new Instance ID token
- String fcmToken = task.getResult().getToken();
- if (fcmToken != null) {
- if (BuildConfig.DEBUG) {
- Log.d(TAG, "Registering with FCM");
- }
- Voice.register(accessToken, Voice.RegistrationChannel.FCM, fcmToken, registrationListener);
+ @ReactMethod //
+ public void unregister(Promise promise) {
+ unregisterForCallInvites();
+ promise.resolve(true);
+ }
+
+ private void unregisterForCallInvites() {
+ FirebaseInstanceId.getInstance().getInstanceId()
+ .addOnCompleteListener(task -> {
+ if (!task.isSuccessful()) {
+ Log.w(TAG, "FCM unregistration failed", task.getException());
+ return;
+ }
+ // Get new Instance ID token
+ String fcmToken = task.getResult().getToken();
+ if (fcmToken != null) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "Unregistering with FCM");
}
+ Voice.unregister(accessToken, Voice.RegistrationChannel.FCM, fcmToken, unregistrationListener);
}
});
}
+ public void acceptFromIntent(Intent intent) {
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "acceptFromIntent()");
+ }
+ activeCallInvite = intent.getParcelableExtra(Constants.INCOMING_CALL_INVITE);
+ if (activeCallInvite == null) {
+ eventManager.sendEvent(EVENT_CALL_INVITE_CANCELLED, null);
+ return;
+ }
+
+ SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging();
+
+ AcceptOptions acceptOptions = new AcceptOptions.Builder()
+ .enableDscp(true)
+ .build();
+ activeCallInvite.accept(getReactApplicationContext(), acceptOptions, callListener);
+ }
+
@ReactMethod
public void accept() {
- callAccepted = true;
SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging();
- if (activeCallInvite != null) {
- if (BuildConfig.DEBUG) {
- Log.d(TAG, "accept()");
- }
- AcceptOptions acceptOptions = new AcceptOptions.Builder()
- .enableDscp(true)
- .build();
- activeCallInvite.accept(getReactApplicationContext(), acceptOptions, callListener);
- clearIncomingNotification(activeCallInvite.getCallSid());
-
- // 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());
-// callNotificationManager.createHangupLocalNotification(getReactApplicationContext(),
-// activeCallInvite.getCallSid(),
-// activeCallInvite.getFrom());
-// eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params);
- } else {
- eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, null);
+ if (activeCallInvite == null) {
+ eventManager.sendEvent(EVENT_CALL_INVITE_CANCELLED, null);
+ return;
+ }
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "accept()");
}
+
+ Intent intent = new Intent(getReactApplicationContext(), IncomingCallNotificationService.class);
+ intent.setAction(Constants.ACTION_JS_ANSWER);
+
+ getReactApplicationContext().startService(intent);
+
+ AcceptOptions acceptOptions = new AcceptOptions.Builder()
+ .enableDscp(true)
+ .build();
+ activeCallInvite.accept(getReactApplicationContext(), acceptOptions, callListener);
}
@ReactMethod
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", "DISCONNECTED");
- // TODO check if DISCONNECTED should be REJECTED
- // params.putString("call_state", "REJECTED");
+ params.putString(Constants.CALL_SID, activeCallInvite.getCallSid());
+ params.putString(Constants.CALL_FROM, activeCallInvite.getFrom());
+ params.putString(Constants.CALL_TO, activeCallInvite.getTo());
activeCallInvite.reject(getReactApplicationContext());
- clearIncomingNotification(activeCallInvite.getCallSid());
}
- eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params);
+
+ Intent intent = new Intent(getReactApplicationContext(), IncomingCallNotificationService.class);
+ intent.setAction(Constants.ACTION_JS_REJECT);
+
+ getReactApplicationContext().startService(intent);
+
+ eventManager.sendEvent(EVENT_CALL_INVITE_CANCELLED, params);
+ activeCallInvite = null;
}
@ReactMethod
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", "BUSY");
- clearIncomingNotification(activeCallInvite.getCallSid());
- }
- eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params);
}
@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) {
- errParams.putString("err", "Invalid access token");
+ errParams.putString(Constants.ERROR, "Invalid access token");
eventManager.sendEvent(EVENT_DEVICE_NOT_READY, errParams);
return;
}
if (params == null) {
- errParams.putString("err", "Invalid parameters");
+ errParams.putString(Constants.ERROR, "Invalid parameters");
eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, errParams);
return;
} else if (!params.hasKey("To")) {
- errParams.putString("err", "Invalid To parameter");
+ errParams.putString(Constants.ERROR, "Invalid To parameter");
eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, errParams);
return;
}
@@ -728,15 +816,15 @@ public void connect(ReadableMap params) {
twiMLParams.put(key, params.getString(key));
break;
default:
- Log.d(TAG, "Could not convert with key: " + key + ".");
+ Log.e(TAG, "Could not convert key: " + key + ". ReadableType: "+ readableType.toString());
break;
}
}
ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken)
- .enableDscp(true)
- .params(twiMLParams)
- .build();
+ .enableDscp(true)
+ .params(twiMLParams)
+ .build();
activeCall = Voice.connect(getReactApplicationContext(), connectOptions, callListener);
}
@@ -765,46 +853,39 @@ public void sendDigits(String digits) {
@ReactMethod
public void getActiveCall(Promise promise) {
- if (activeCall != null) {
- if (BuildConfig.DEBUG) {
- Log.d(TAG, "Active call found state = "+activeCall.getState());
- }
- WritableMap params = Arguments.createMap();
- String toNum = activeCall.getTo();
- if (toNum == null) {
- toNum = toNumber;
- }
- params.putString("call_sid", activeCall.getSid());
- params.putString("call_from", activeCall.getFrom());
- params.putString("call_to", toNum);
- params.putString("call_state", activeCall.getState().name());
- promise.resolve(params);
+ if (activeCall == null) {
+ promise.resolve(null);
return;
}
- promise.resolve(null);
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "getActiveCall(). Active call state: " + activeCall.getState());
+ }
+ WritableMap params = Arguments.createMap();
+ String toNum = activeCall.getTo();
+ if (toNum == null) {
+ toNum = toNumber;
+ }
+ params.putString(Constants.CALL_SID, activeCall.getSid());
+ params.putString(Constants.CALL_FROM, activeCall.getFrom());
+ params.putString(Constants.CALL_TO, toNum);
+ params.putString(Constants.CALL_STATE, activeCall.getState().name());
+ promise.resolve(params);
}
@ReactMethod
public void getCallInvite(Promise promise) {
- if (activeCallInvite != null) {
- if (BuildConfig.DEBUG) {
- Log.d(TAG, "Call invite found "+ activeCallInvite);
- }
- WritableMap params = Arguments.createMap();
- params.putString("call_sid", activeCallInvite.getCallSid());
- params.putString("call_from", activeCallInvite.getFrom());
- params.putString("call_to", activeCallInvite.getTo());
- promise.resolve(params);
+ if (activeCallInvite == null) {
+ promise.resolve(null);
return;
}
- promise.resolve(null);
- }
-
- @ReactMethod
- public void setSpeakerPhone(Boolean value) {
- // TODO check whether it is necessary to call setAudioFocus again
-// setAudioFocus();
- audioManager.setSpeakerphoneOn(value);
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "getCallInvite(). Call invite: " + activeCallInvite);
+ }
+ WritableMap params = Arguments.createMap();
+ params.putString(Constants.CALL_SID, activeCallInvite.getCallSid());
+ params.putString(Constants.CALL_FROM, activeCallInvite.getFrom());
+ params.putString(Constants.CALL_TO, activeCallInvite.getTo());
+ promise.resolve(params);
}
@ReactMethod
@@ -814,59 +895,44 @@ public void setOnHold(Boolean value) {
}
}
- private void setAudioFocus() {
- if (audioManager == null) {
- audioManager.setMode(originalAudioMode);
- audioManager.abandonAudioFocus(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 {
- int focusRequestResult = audioManager.requestAudioFocus(new AudioManager.OnAudioFocusChangeListener() {
- @Override
- public void onAudioFocusChange(int focusChange) {}
- },
- AudioManager.STREAM_VOICE_CALL,
- AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
+ @ReactMethod
+ public void getAudioDevices(Promise promise) {
+ List availableAudioDevices = audioSwitch.getAvailableAudioDevices();
+
+ WritableMap devices = Arguments.createMap();
+ for (AudioDevice a : availableAudioDevices) {
+ devices.putBoolean(a.getName(), selectedAudioDevice.getName().equals(a.getName()));
}
- /*
- * 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);
+ promise.resolve(devices);
}
- private void unsetAudioFocus() {
- if (audioManager == null) {
- audioManager.setMode(originalAudioMode);
- audioManager.abandonAudioFocus(null);
+ @ReactMethod
+ public void getSelectedAudioDevice(Promise promise) {
+ WritableMap device = Arguments.createMap();
+ device.putString(Constants.SELECTED_AUDIO_DEVICE, selectedAudioDevice.getName());
+ promise.resolve(device);
+ }
+
+ @ReactMethod
+ public void selectAudioDevice(String name) {
+ AudioDevice selected = availableAudioDevices.get(name);
+ if (selected == null) {
return;
}
- audioManager.setMode(originalAudioMode);
- if (Build.VERSION.SDK_INT >= 26) {
- if (focusRequest != null) {
- audioManager.abandonAudioFocusRequest(focusRequest);
+ audioSwitch.selectDevice(selected);
+ }
+
+ private void startAudioSwitch() {
+ audioSwitch.start((devices, device) -> {
+ selectedAudioDevice = device;
+ WritableMap params = Arguments.createMap();
+ for (AudioDevice a : devices) {
+ params.putBoolean(a.getName(), device.getName().equals(a.getName()));
+ availableAudioDevices.put(a.getName(), a);
}
- } else {
- audioManager.abandonAudioFocus(null);
- }
+ eventManager.sendEvent(EVENT_AUDIO_DEVICES_UPDATED, params);
+ return Unit.INSTANCE;
+ });
}
private boolean checkPermissionForMicrophone() {
@@ -879,11 +945,35 @@ private void requestPermissionForMicrophone() {
return;
}
if (ActivityCompat.shouldShowRequestPermissionRationale(getCurrentActivity(), Manifest.permission.RECORD_AUDIO)) {
-// Snackbar.make(coordinatorLayout,
-// "Microphone permissions needed. Please allow in your application settings.",
-// SNACKBAR_DURATION).show();
+ // TODO
} else {
ActivityCompat.requestPermissions(getCurrentActivity(), new String[]{Manifest.permission.RECORD_AUDIO}, MIC_PERMISSION_REQUEST_CODE);
}
}
+
+ public static Bundle getActivityLaunchOption(Intent intent) {
+ Bundle initialProperties = new Bundle();
+ if (intent == null || intent.getAction() == null) {
+ return initialProperties;
+ }
+
+ Bundle callBundle = new Bundle();
+ switch (intent.getAction()) {
+ case Constants.ACTION_INCOMING_CALL_NOTIFICATION:
+ callBundle.putString(Constants.CALL_SID, intent.getStringExtra(Constants.CALL_SID));
+ callBundle.putString(Constants.CALL_FROM, intent.getStringExtra(Constants.CALL_FROM));
+ callBundle.putString(Constants.CALL_TO, intent.getStringExtra(Constants.CALL_TO));
+ initialProperties.putBundle(Constants.CALL_INVITE_KEY, callBundle);
+ break;
+
+ case Constants.ACTION_ACCEPT:
+ callBundle.putString(Constants.CALL_SID, intent.getStringExtra(Constants.CALL_SID));
+ callBundle.putString(Constants.CALL_FROM, intent.getStringExtra(Constants.CALL_FROM));
+ callBundle.putString(Constants.CALL_TO, intent.getStringExtra(Constants.CALL_TO));
+ callBundle.putString(Constants.CALL_STATE, Constants.CALL_STATE_CONNECTED);
+ initialProperties.putBundle(Constants.CALL_KEY, callBundle);
+ break;
+ }
+ return initialProperties;
+ }
}
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 520b77f1..15dd4395 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,6 +4,8 @@
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.util.Log;
@@ -14,6 +16,9 @@
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
+import com.hoxfon.react.RNTwilioVoice.Constants;
+import com.hoxfon.react.RNTwilioVoice.IncomingCallNotificationService;
+import com.twilio.voice.CallException;
import com.hoxfon.react.RNTwilioVoice.BuildConfig;
import com.hoxfon.react.RNTwilioVoice.CallNotificationManager;
import com.twilio.voice.CallInvite;
@@ -25,30 +30,18 @@
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_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;
public class VoiceFirebaseMessagingService extends FirebaseMessagingService {
- private CallNotificationManager callNotificationManager;
-
@Override
public void onCreate() {
super.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);
+ super.onNewToken(token);
+ Intent intent = new Intent(Constants.ACTION_FCM_TOKEN);
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
@@ -71,76 +64,53 @@ 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(this, data, new MessageListener() {
@Override
public void onCallInvite(final CallInvite callInvite) {
-
// We need to run this on the main thread, as the React code assumes that is true.
// Namely, DevServerHelper constructs a Handler() without a Looper, which triggers:
// "Can't create handler inside thread that has not called Looper.prepare()"
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
public void run() {
+ CallNotificationManager callNotificationManager = new CallNotificationManager();
// Construct and load our normal React JS code bundle
ReactInstanceManager mReactInstanceManager = ((ReactApplication) getApplication()).getReactNativeHost().getReactInstanceManager();
ReactContext context = mReactInstanceManager.getCurrentReactContext();
- // If it's constructed, send a notification
+
+ // initialise appImportance to the highest possible importance in case context is null
+ int appImportance = ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE;
+
if (context != null) {
- int appImportance = callNotificationManager.getApplicationImportance((ReactApplicationContext)context);
+ appImportance = callNotificationManager.getApplicationImportance((ReactApplicationContext)context);
+ }
+ if (BuildConfig.DEBUG) {
+ Log.d(TAG, "context: " + context + ". appImportance = " + appImportance);
+ }
+
+ // when the app is not started or in the background
+ if (appImportance > ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE) {
if (BuildConfig.DEBUG) {
- Log.d(TAG, "CONTEXT present appImportance = " + appImportance);
- }
- Intent launchIntent = callNotificationManager.getLaunchIntent(
- (ReactApplicationContext)context,
- notificationId,
- callInvite,
- false,
- appImportance
- );
- // app is not in foreground
- if (appImportance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
- context.startActivity(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() {
- public void onReactContextInitialized(ReactContext 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);
- context.startActivity(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()) {
- // Construct it in the background
- mReactInstanceManager.createReactContextInBackground();
+ Log.d(TAG, "Background");
}
+ handleInvite(callInvite, notificationId);
+ return;
}
+
+ Intent intent = new Intent(Constants.ACTION_INCOMING_CALL);
+ intent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);
+ intent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite);
+ LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
});
}
@Override
- public void onCancelledCallInvite(final CancelledCallInvite cancelledCallInvite) {
- Handler handler = new Handler(Looper.getMainLooper());
- handler.post(new Runnable() {
- public void run() {
- VoiceFirebaseMessagingService.this.sendCancelledCallInviteToActivity(cancelledCallInvite);
- }
- });
+ public void onCancelledCallInvite(@NonNull CancelledCallInvite cancelledCallInvite, @Nullable CallException callException) {
+ // The call is prematurely disconnected by the caller.
+ // The callee does not accept or reject the call within 30 seconds.
+ // The Voice SDK is unable to establish a connection to Twilio.
+ handleCancelledCallInvite(cancelledCallInvite, callException);
}
});
@@ -155,13 +125,22 @@ public void run() {
}
}
- /*
- * Send the CancelledCallInvite to the TwilioVoiceModule
- */
- private void sendCancelledCallInviteToActivity(CancelledCallInvite cancelledCallInvite) {
- SoundPoolManager.getInstance((this)).stopRinging();
- Intent intent = new Intent(ACTION_CANCEL_CALL_INVITE);
- intent.putExtra(CANCELLED_CALL_INVITE, cancelledCallInvite);
- LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+ private void handleInvite(CallInvite callInvite, int notificationId) {
+ Intent intent = new Intent(this, IncomingCallNotificationService.class);
+ intent.setAction(Constants.ACTION_INCOMING_CALL);
+ intent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);
+ intent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite);
+
+ startService(intent);
+ }
+
+ private void handleCancelledCallInvite(CancelledCallInvite cancelledCallInvite, CallException callException) {
+ Intent intent = new Intent(this, IncomingCallNotificationService.class);
+ intent.setAction(Constants.ACTION_CANCEL_CALL);
+ intent.putExtra(Constants.CANCELLED_CALL_INVITE, cancelledCallInvite);
+ if (callException != null) {
+ intent.putExtra(Constants.CANCELLED_CALL_INVITE_EXCEPTION, callException.getMessage());
+ }
+ startService(intent);
}
}
diff --git a/android/src/main/res/values/style.xml b/android/src/main/res/values/style.xml
new file mode 100644
index 00000000..fc100b88
--- /dev/null
+++ b/android/src/main/res/values/style.xml
@@ -0,0 +1,5 @@
+
+
+ #d6312e
+ #258c42
+
\ No newline at end of file
diff --git a/android/src/main/res/values/values.xml b/android/src/main/res/values/values.xml
new file mode 100644
index 00000000..e89d98e7
--- /dev/null
+++ b/android/src/main/res/values/values.xml
@@ -0,0 +1,13 @@
+
+
+ Answer
+ Decline
+ Hang up
+ Call in progress
+ Incoming call
+ is calling
+ Missed call
+ last call from:
+ called
+ missed calls
+
diff --git a/index.js b/index.js
index b84b91bf..e472949b 100644
--- a/index.js
+++ b/index.js
@@ -22,6 +22,7 @@ const _eventHandlers = {
callStateRinging: new Map(),
callInviteCancelled: new Map(),
callRejected: new Map(),
+ audioDevicesUpdated: new Map(),
}
const Twilio = {
@@ -70,7 +71,12 @@ const Twilio = {
TwilioVoice.ignore()
},
setMuted: TwilioVoice.setMuted,
- setSpeakerPhone: TwilioVoice.setSpeakerPhone,
+ setSpeakerPhone(value) {
+ if (Platform.OS === ANDROID) {
+ return
+ }
+ return TwilioVoice.setSpeakerPhone(value)
+ },
sendDigits: TwilioVoice.sendDigits,
hold: TwilioVoice.setOnHold,
requestPermissions(senderId) {
@@ -86,9 +92,32 @@ const Twilio = {
}
},
unregister() {
+ TwilioVoice.unregister()
+ },
+ // getAudioDevices returns all audio devices connected
+ // {
+ // "Speakerphone": false,
+ // "Earnpiece": true, // true indicates the selected audio device
+ // }
+ getAudioDevices() {
if (Platform.OS === IOS) {
- TwilioVoice.unregister()
+ return
}
+ TwilioVoice.getAudioDevices()
+ },
+ // getSelectedAudioDevice returns the selected audio device
+ getSelectedAudioDevice() {
+ if (Platform.OS === IOS) {
+ return
+ }
+ TwilioVoice.getSelectedAudioDevice()
+ },
+ // selectAudioDevice selects the passed audio device for the current active call
+ selectAudioDevice(name: string) {
+ if (Platform.OS === IOS) {
+ return
+ }
+ TwilioVoice.selectAudioDevice(name)
},
addEventListener(type, handler) {
if (!_eventHandlers.hasOwnProperty(type)) {
@@ -106,7 +135,7 @@ const Twilio = {
}
_eventHandlers[type].get(handler).remove()
_eventHandlers[type].delete(handler)
- }
+ },
}
export default Twilio
diff --git a/ios/RNTwilioVoice/RNTwilioVoice.m b/ios/RNTwilioVoice/RNTwilioVoice.m
index df4c3810..1c8a96b9 100644
--- a/ios/RNTwilioVoice/RNTwilioVoice.m
+++ b/ios/RNTwilioVoice/RNTwilioVoice.m
@@ -13,7 +13,6 @@ @interface RNTwilioVoice ()