diff --git a/README.md b/README.md index a0b7e2b4..07d3fd67 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This is a React-Native wrapper for [Twilio Programmable Voice SDK](https://www.t ## Twilio Programmable Voice SDK -- Android 4.5.0 (bundled within the module) +- Android 5.0.0 (bundled within the module) - iOS 5.1.0 (specified by the app's own podfile) ## Breaking changes in v4.0.0 diff --git a/android/build.gradle b/android/build.gradle index fe752fa4..8be3bc3c 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -55,9 +55,10 @@ dependencies { def supportLibVersion = rootProject.hasProperty('supportLibVersion') ? rootProject.supportLibVersion : DEFAULT_SUPPORT_LIB_VERSION implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.twilio:voice-android:4.5.0' + implementation 'com.twilio:voice-android:5.0.2' implementation "com.android.support:appcompat-v7:$supportLibVersion" implementation 'com.facebook.react:react-native:+' implementation 'com.google.firebase:firebase-messaging:17.6.+' + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' 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/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java index af4e9175..37b95c7f 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/CallNotificationManager.java @@ -26,25 +26,23 @@ import java.util.List; import static android.content.Context.ACTIVITY_SERVICE; - +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_ANSWER_CALL; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_REJECT_CALL; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_HANGUP_CALL; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_INCOMING_CALL; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_MISSED_CALL; +import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_CALL_INVITE; +import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_CALL_NOTIFICATION_ID; +import static com.hoxfon.react.RNTwilioVoice.Constants.NOTIFICATION_TYPE; +import static com.hoxfon.react.RNTwilioVoice.Constants.CALL_SID_KEY; +import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_NOTIFICATION_PREFIX; +import static com.hoxfon.react.RNTwilioVoice.Constants.MISSED_CALLS_GROUP; +import static com.hoxfon.react.RNTwilioVoice.Constants.MISSED_CALLS_NOTIFICATION_ID; +import static com.hoxfon.react.RNTwilioVoice.Constants.HANGUP_NOTIFICATION_ID; +import static com.hoxfon.react.RNTwilioVoice.Constants.PREFERENCE_KEY; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_CLEAR_MISSED_CALLS_COUNT; +import static com.hoxfon.react.RNTwilioVoice.Constants.CLEAR_MISSED_CALLS_NOTIFICATION_ID; 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 +70,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,104 +82,110 @@ 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) - { - if (BuildConfig.DEBUG) { - Log.d(TAG, "createIncomingCallNotification intent "+launchIntent.getFlags()); - } - 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 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; +// +// launchIntent.setAction(ACTION_INCOMING_CALL) +// .putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId) +//// .addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) +// .addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) +// .addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) +//// .addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP) +// ; +// if (shouldStartNewTask || appImportance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { +// launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); +// } +// +//// .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) +// { +// if (BuildConfig.DEBUG) { +// Log.d(TAG, "createIncomingCallNotification intent "+launchIntent.getFlags()); +// } +// 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) { @@ -194,7 +198,7 @@ public void initCallNotificationsChannel(NotificationManager notificationManager notificationManager.createNotificationChannel(channel); } - public void createMissedCallNotification(ReactApplicationContext context, CallInvite callInvite) { + public void createMissedCallNotification(ReactApplicationContext context, String callSid, String callFrom) { SharedPreferences sharedPref = context.getSharedPreferences(PREFERENCE_KEY, Context.MODE_PRIVATE); SharedPreferences.Editor sharedPrefEditor = sharedPref.edit(); @@ -216,7 +220,7 @@ public void createMissedCallNotification(ReactApplicationContext context, CallIn */ Bundle extras = new Bundle(); extras.putInt(INCOMING_CALL_NOTIFICATION_ID, MISSED_CALLS_NOTIFICATION_ID); - extras.putString(CALL_SID_KEY, callInvite.getCallSid()); + extras.putString(CALL_SID_KEY, callSid); extras.putString(NOTIFICATION_TYPE, ACTION_MISSED_CALL); /* @@ -231,7 +235,7 @@ public void createMissedCallNotification(ReactApplicationContext context, CallIn .setCategory(NotificationCompat.CATEGORY_MESSAGE) .setSmallIcon(R.drawable.ic_call_missed_white_24dp) .setContentTitle("Missed call") - .setContentText(callInvite.getFrom() + " called") + .setContentText(callFrom + " called") .setAutoCancel(true) .setShowWhen(true) .setExtras(extras) @@ -246,7 +250,7 @@ public void createMissedCallNotification(ReactApplicationContext context, CallIn } else { inboxStyle.setBigContentTitle(String.valueOf(missedCalls) + " missed calls"); } - inboxStyle.addLine("from: " +callInvite.getFrom()); + inboxStyle.addLine("from: " +callFrom); sharedPrefEditor.putInt(MISSED_CALLS_GROUP, missedCalls); sharedPrefEditor.commit(); 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..24786c3a --- /dev/null +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/Constants.java @@ -0,0 +1,33 @@ +package com.hoxfon.react.RNTwilioVoice; + +public class Constants { + 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.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 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_ANSWER_CALL = "ANSWER_CALL"; + public static final String ACTION_REJECT_CALL = "REJECT_CALL"; + public static final String ACTION_HANGUP_CALL = "HANGUP_CALL"; + public static final String ACTION_INCOMING_CALL_NOTIFICATION = "ACTION_INCOMING_CALL_NOTIFICATION"; + public static final String ACTION_INCOMING_CALL = "ACTION_INCOMING_CALL"; + 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_CANCEL_CALL_INVITE = "CANCEL_CALL_INVITE"; + public static final String ACTION_CLEAR_MISSED_CALLS_COUNT = "CLEAR_MISSED_CALLS_COUNT"; + + public static final String NOTIFICATION_TYPE = "NOTIFICATION_TYPE"; + public static final String CANCELLED_CALL_INVITE_ERR = "CANCELLED_CALL_INVITE_EXCEPTION"; + +} 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..4e6f459f --- /dev/null +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java @@ -0,0 +1,234 @@ +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.graphics.Color; +import android.os.Build; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; + +import androidx.core.app.NotificationCompat; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.ProcessLifecycleOwner; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.WritableMap; +import com.twilio.voice.CallInvite; + +import static com.hoxfon.react.RNTwilioVoice.CallNotificationManager.getMainActivityClass; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_ACCEPT; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_CANCEL_CALL; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_INCOMING_CALL; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_INCOMING_CALL_NOTIFICATION; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_REJECT; +import static com.hoxfon.react.RNTwilioVoice.Constants.CALL_SID_KEY; +import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_CALL_INVITE; +import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_CALL_NOTIFICATION_ID; +import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CONNECTION_DID_DISCONNECT; +import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG; + +public class IncomingCallNotificationService extends Service { + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + String action = intent.getAction(); + + CallInvite callInvite = intent.getParcelableExtra(INCOMING_CALL_INVITE); + int notificationId = intent.getIntExtra(INCOMING_CALL_NOTIFICATION_ID, 0); + + switch (action) { + case ACTION_INCOMING_CALL: + handleIncomingCall(callInvite, notificationId); + break; + case ACTION_ACCEPT: + accept(callInvite, notificationId); + break; + case ACTION_REJECT: + reject(callInvite); + break; + case ACTION_CANCEL_CALL: + handleCancelledCall(intent); + break; + default: + break; + } + return START_NOT_STICKY; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private Notification createNotification(CallInvite callInvite, int notificationId, int channelImportance) { + Intent intent = new Intent(this, getMainActivityClass(this)); + intent.setAction(ACTION_INCOMING_CALL_NOTIFICATION); + intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); + intent.putExtra(INCOMING_CALL_INVITE, callInvite); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + 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(CALL_SID_KEY, callInvite.getCallSid()); + + String contextText = callInvite.getFrom() + " is calling."; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + // TODO make text configurable from app resources + return buildNotification(contextText, + pendingIntent, + extras, + callInvite, + notificationId, + createChannel(channelImportance)); + } else { + // TODO make text configurable from app resources + return new NotificationCompat.Builder(this) + .setSmallIcon(R.drawable.ic_call_white_24dp) + .setContentTitle("Incoming call") + .setContentText(contextText) + .setAutoCancel(true) + .setExtras(extras) + .setContentIntent(pendingIntent) + .setGroup("test_app_notification") + .setColor(Color.rgb(214, 10, 37)).build(); + } + } + + /** + * 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) { + Intent rejectIntent = new Intent(getApplicationContext(), IncomingCallNotificationService.class); + rejectIntent.setAction(ACTION_REJECT); + rejectIntent.putExtra(INCOMING_CALL_INVITE, callInvite); + rejectIntent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); + PendingIntent piRejectIntent = PendingIntent.getService(getApplicationContext(), 0, rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + Intent acceptIntent = new Intent(getApplicationContext(), IncomingCallNotificationService.class); + acceptIntent.setAction(ACTION_ACCEPT); + acceptIntent.putExtra(INCOMING_CALL_INVITE, callInvite); + acceptIntent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); + PendingIntent piAcceptIntent = PendingIntent.getService(getApplicationContext(), 0, acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + Notification.Builder builder = + new Notification.Builder(getApplicationContext(), channelId) + .setSmallIcon(R.drawable.ic_call_white_24dp) + .setContentTitle("Incoming call") + .setContentText(text) + .setCategory(Notification.CATEGORY_CALL) + .setExtras(extras) + .setAutoCancel(true) + .addAction(android.R.drawable.ic_menu_delete, getString(R.string.decline), piRejectIntent) + .addAction(android.R.drawable.ic_menu_call, getString(R.string.answer), piAcceptIntent) + .setFullScreenIntent(pendingIntent, true); + + return builder.build(); + } + + @TargetApi(Build.VERSION_CODES.O) + private String createChannel(int channelImportance) { + NotificationChannel callInviteChannel = new NotificationChannel(Constants.VOICE_CHANNEL_HIGH_IMPORTANCE, + "Primary Voice Channel", NotificationManager.IMPORTANCE_HIGH);; + String channelId = Constants.VOICE_CHANNEL_HIGH_IMPORTANCE; + + if (channelImportance == NotificationManager.IMPORTANCE_LOW) { + callInviteChannel = new NotificationChannel(Constants.VOICE_CHANNEL_LOW_IMPORTANCE, + "Primary Voice Channel", NotificationManager.IMPORTANCE_LOW);; + channelId = Constants.VOICE_CHANNEL_LOW_IMPORTANCE; + } + callInviteChannel.setLightColor(Color.GREEN); + 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)); +// Intent activeCallIntent = new Intent(this, TwilioVoiceModule.class); + activeCallIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + activeCallIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activeCallIntent.putExtra(INCOMING_CALL_INVITE, callInvite); + activeCallIntent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); + activeCallIntent.setAction(ACTION_ACCEPT); + startActivity(activeCallIntent); + } + + private void reject(CallInvite callInvite) { + endForeground(); + callInvite.reject(getApplicationContext()); + } + + private void handleCancelledCall(Intent intent) { + 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); + } + + private void setCallInProgressNotification(CallInvite callInvite, int notificationId) { + int importance = NotificationManager.IMPORTANCE_LOW; + if (!isAppVisible()) { + Log.i(TAG, "setCallInProgressNotification - app is NOT visible."); + importance = NotificationManager.IMPORTANCE_HIGH; + } + 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !isAppVisible()) { + return; + } + Intent intent = new Intent(this, getMainActivityClass(this)); + intent.setAction(ACTION_INCOMING_CALL); + intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); + intent.putExtra(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/TwilioVoiceModule.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java index fb68f49d..4de31091 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java @@ -39,10 +39,7 @@ 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.voice.AcceptOptions; import com.twilio.voice.Call; import com.twilio.voice.CallException; @@ -57,13 +54,24 @@ import java.util.HashMap; import java.util.Map; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_ACCEPT; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_CANCEL_CALL; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_FCM_TOKEN; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_INCOMING_CALL; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_INCOMING_CALL_NOTIFICATION; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_MISSED_CALL; +import static com.hoxfon.react.RNTwilioVoice.Constants.CANCELLED_CALL_INVITE; +import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_CALL_INVITE; +import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_CALL_NOTIFICATION_ID; +import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_NOTIFICATION_PREFIX; +import static com.hoxfon.react.RNTwilioVoice.Constants.MISSED_CALLS_GROUP; +import static com.hoxfon.react.RNTwilioVoice.Constants.PREFERENCE_KEY; import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CONNECTION_DID_CONNECT; import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CONNECTION_DID_DISCONNECT; import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_DEVICE_DID_RECEIVE_INCOMING; import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_DEVICE_NOT_READY; import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_DEVICE_READY; import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CALL_STATE_RINGING; -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; @@ -74,7 +82,7 @@ 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 int savedAudioMode = AudioManager.MODE_NORMAL; private boolean isReceiverRegistered = false; private VoiceBroadcastReceiver voiceBroadcastReceiver; @@ -82,30 +90,6 @@ public class TwilioVoiceModule extends ReactContextBaseJavaModule implements Act // 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; @@ -122,6 +106,7 @@ public class TwilioVoiceModule extends ReactContextBaseJavaModule implements Act private CallInvite activeCallInvite; private Call activeCall; + private int activeCallNotificationId; // this variable determines when to create missed calls notifications private Boolean callAccepted = false; @@ -391,61 +376,81 @@ public void onConnectFailure(Call call, CallException error) { * Register the Voice broadcast receiver */ private void registerReceiver() { - if (!isReceiverRegistered) { - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(ACTION_INCOMING_CALL); - intentFilter.addAction(ACTION_CANCEL_CALL_INVITE); - intentFilter.addAction(ACTION_MISSED_CALL); - LocalBroadcastManager.getInstance(getReactApplicationContext()).registerReceiver( - voiceBroadcastReceiver, intentFilter); - registerActionReceiver(); - isReceiverRegistered = true; + if (isReceiverRegistered) { + return; } + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(ACTION_INCOMING_CALL); + intentFilter.addAction(ACTION_CANCEL_CALL); + intentFilter.addAction(ACTION_FCM_TOKEN); + intentFilter.addAction(ACTION_MISSED_CALL); + LocalBroadcastManager.getInstance(getReactApplicationContext()).registerReceiver( + voiceBroadcastReceiver, intentFilter); +// registerActionReceiver(); + isReceiverRegistered = true; } -// private void unregisterReceiver() { -// if (isReceiverRegistered) { -// LocalBroadcastManager.getInstance(getReactApplicationContext()).unregisterReceiver(voiceBroadcastReceiver); -// isReceiverRegistered = false; -// } -// } - - private void registerActionReceiver() { + private void unregisterReceiver() { + if (!isReceiverRegistered) { + return; + } + LocalBroadcastManager.getInstance(getReactApplicationContext()).unregisterReceiver(voiceBroadcastReceiver); + isReceiverRegistered = false; + } - 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); + private class VoiceBroadcastReceiver extends BroadcastReceiver { - getReactApplicationContext().registerReceiver(new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - switch (action) { - case ACTION_ANSWER_CALL: - accept(); - break; - case ACTION_REJECT_CALL: - reject(); - break; - case 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(); - } - // 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)); + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "VoiceBroadcastReceiver.onReceive "+action+". Intent "+ intent.getExtras()); } - }, intentFilter); + if (action.equals(ACTION_INCOMING_CALL) || action.equals(ACTION_CANCEL_CALL)) { + /* + * Handle the incoming or cancelled call invite + */ + handleIncomingCallIntent(intent); + } + } } +// 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); +// +// getReactApplicationContext().registerReceiver(new BroadcastReceiver() { +// @Override +// public void onReceive(Context context, Intent intent) { +// String action = intent.getAction(); +// switch (action) { +//// case ACTION_ANSWER_CALL: +//// accept(); +//// break; +//// case ACTION_REJECT_CALL: +//// reject(); +//// break; +// case 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(); +// } +// // 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); +// } + // removed @Override temporarily just to get it working on different versions of RN public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) { onActivityResult(requestCode, resultCode, data); @@ -457,97 +462,127 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } private void handleIncomingCallIntent(Intent intent) { - if (intent.getAction().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 (intent == null || intent.getAction() == null) { + return; + } + String action = intent.getAction(); + activeCallInvite = intent.getParcelableExtra(INCOMING_CALL_INVITE); + activeCallNotificationId = intent.getIntExtra(INCOMING_CALL_NOTIFICATION_ID, 0); - if (getReactApplicationContext().getCurrentActivity() != null) { - Window window = getReactApplicationContext().getCurrentActivity().getWindow(); - window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED - ); - } - // 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); - } - } else { - // TODO evaluate what more is needed at this point? - Log.e(TAG, "ACTION_INCOMING_CALL but not active call"); - } - } else if (intent.getAction().equals(ACTION_CANCEL_CALL_INVITE)) { - SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); - if (BuildConfig.DEBUG) { - Log.d(TAG, "activeCallInvite was cancelled by " + activeCallInvite.getFrom()); - } - if (!callAccepted) { + switch (action) { + case ACTION_INCOMING_CALL: + handleIncomingCall(); + break; + + case ACTION_INCOMING_CALL_NOTIFICATION: if (BuildConfig.DEBUG) { - Log.d(TAG, "creating a missed call"); - } - callNotificationManager.createMissedCallNotification(getReactApplicationContext(), activeCallInvite); - int appImportance = callNotificationManager.getApplicationImportance(getReactApplicationContext()); - if (appImportance != RunningAppProcessInfo.IMPORTANCE_BACKGROUND) { - WritableMap params = Arguments.createMap(); - params.putString("call_sid", activeCallInvite.getCallSid()); - params.putString("call_from", activeCallInvite.getFrom()); - params.putString("call_to", activeCallInvite.getTo()); - params.putString("call_state", Call.State.DISCONNECTED.toString()); - eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); + Log.d(TAG, "ACTION_INCOMING_CALL_NOTIFICATION"); } - } - clearIncomingNotification(activeCallInvite.getCallSid()); - } else if (intent.getAction().equals(ACTION_FCM_TOKEN)) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "handleIncomingCallIntent ACTION_FCM_TOKEN"); - } - registerForCallInvites(); - } - } + break; - private class VoiceBroadcastReceiver extends BroadcastReceiver { + case ACTION_CANCEL_CALL: + handleCancel(intent); + break; - @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)) { + case 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 { + break; + + case ACTION_FCM_TOKEN: + if (BuildConfig.DEBUG) { + Log.d(TAG, "handleIncomingCallIntent ACTION_FCM_TOKEN"); + } + registerForCallInvites(); + break; + + case ACTION_ACCEPT: + accept(); + break; + + default: Log.e(TAG, "received broadcast unhandled action " + action); - } + break; + } + } + + private void handleIncomingCall() { + if (BuildConfig.DEBUG) { + Log.d(TAG, "handleIncomingCallIntent"); + } + if (activeCallInvite == null) { + // TODO evaluate what more is needed at this point? + Log.e(TAG, "ACTION_INCOMING_CALL but not active call"); + return; + } + 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 + ); + } + // send a JS event ONLY if the app is VISIBLE + // at startup the app would try to fetch the activeIncoming calls + int appImportance = callNotificationManager.getApplicationImportance(getReactApplicationContext()); + if (appImportance <= RunningAppProcessInfo.IMPORTANCE_VISIBLE) { + WritableMap params = Arguments.createMap(); + params.putString("call_sid", activeCallInvite.getCallSid()); + params.putString("call_from", activeCallInvite.getFrom()); + params.putString("call_to", activeCallInvite.getTo()); + eventManager.sendEvent(EVENT_DEVICE_DID_RECEIVE_INCOMING, params); } } + private void handleCancel(Intent intent) { + CancelledCallInvite cancelledCallInvite = intent.getParcelableExtra(CANCELLED_CALL_INVITE); + + SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); + if (BuildConfig.DEBUG) { + Log.d(TAG, "activeCallInvite was cancelled by " + cancelledCallInvite.getFrom()); + } + if (callAccepted) { + clearIncomingNotification(cancelledCallInvite.getCallSid()); + return; + } + if (BuildConfig.DEBUG) { + Log.d(TAG, "creating a missed call"); + } + callNotificationManager.createMissedCallNotification( + getReactApplicationContext(), + cancelledCallInvite.getCallSid(), + cancelledCallInvite.getFrom() + ); + // send a JS event ONLY if the app is VISIBLE + int appImportance = callNotificationManager.getApplicationImportance(getReactApplicationContext()); + if (appImportance <= RunningAppProcessInfo.IMPORTANCE_VISIBLE) { + WritableMap params = Arguments.createMap(); + params.putString("call_sid", cancelledCallInvite.getCallSid()); + params.putString("call_from", cancelledCallInvite.getFrom()); + params.putString("call_to", cancelledCallInvite.getTo()); + params.putString("call_state", Call.State.DISCONNECTED.toString()); + eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); + } + + +// String cancelledCallInviteErr = intent.getParcelableExtra(CANCELLED_CALL_INVITE_ERR); +// 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()); +// if (cancelledCallInviteErr != "") { +// params.putString("err", cancelledCallInviteErr); +// } +// } +// eventManager.sendEvent(EVENT_CALL_INVITE_CANCELLED, params); + } + @ReactMethod public void initWithAccessToken(final String accessToken, Promise promise) { if (accessToken.equals("")) { @@ -593,75 +628,64 @@ 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; - } - - // 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); - } - } - }); + final 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); } @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 { + if (activeCallInvite == null) { eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, null); + return; } - } + if (BuildConfig.DEBUG) { + Log.d(TAG, "accept()"); + } + AcceptOptions acceptOptions = new AcceptOptions.Builder() + .enableDscp(true) + .build(); + activeCallInvite.accept(getReactApplicationContext(), acceptOptions, callListener); + clearIncomingNotification(activeCallInvite.getCallSid()); - @ReactMethod - public void reject() { - callAccepted = false; - SoundPoolManager.getInstance(getReactApplicationContext()).stopRinging(); + // 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(); - 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"); - activeCallInvite.reject(getReactApplicationContext()); - clearIncomingNotification(activeCallInvite.getCallSid()); - } - eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); - } + 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); + } + +// @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"); +// activeCallInvite.reject(getReactApplicationContext()); +// clearIncomingNotification(activeCallInvite.getCallSid()); +// } +// eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params); +// } @ReactMethod public void ignore() { @@ -812,18 +836,18 @@ public void setOnHold(Boolean value) { private void setAudioFocus() { if (audioManager == null) { - audioManager.setMode(originalAudioMode); + audioManager.setMode(savedAudioMode); audioManager.abandonAudioFocus(null); return; } - originalAudioMode = audioManager.getMode(); + savedAudioMode = 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) + focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT) .setAudioAttributes(playbackAttributes) .setAcceptsDelayedFocusGain(true) .setOnAudioFocusChangeListener(new AudioManager.OnAudioFocusChangeListener() { @@ -851,11 +875,11 @@ public void onAudioFocusChange(int focusChange) {} private void unsetAudioFocus() { if (audioManager == null) { - audioManager.setMode(originalAudioMode); + audioManager.setMode(savedAudioMode); audioManager.abandonAudioFocus(null); return; } - audioManager.setMode(originalAudioMode); + audioManager.setMode(savedAudioMode); if (Build.VERSION.SDK_INT >= 26) { if (focusRequest != null) { audioManager.abandonAudioFocusRequest(focusRequest); 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..9f562b33 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,8 @@ import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.RemoteMessage; +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; @@ -24,23 +28,23 @@ import java.util.Map; import java.util.Random; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_CANCEL_CALL; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_FCM_TOKEN; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_INCOMING_CALL; +import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_CANCEL_CALL_INVITE; +import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_CALL_INVITE; +import static com.hoxfon.react.RNTwilioVoice.Constants.CANCELLED_CALL_INVITE; +import static com.hoxfon.react.RNTwilioVoice.Constants.CANCELLED_CALL_INVITE_ERR; +import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_CALL_NOTIFICATION_ID; 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 @@ -71,76 +75,82 @@ 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 - if (context != null) { - int appImportance = callNotificationManager.getApplicationImportance((ReactApplicationContext)context); - 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(); - } + + // if the app is closed or not visible, create a heads-up notification + int appImportance = callNotificationManager.getApplicationImportance((ReactApplicationContext)context); + if (BuildConfig.DEBUG) { + Log.d(TAG, "CONTEXT present appImportance = " + appImportance); + } + if (context == null || appImportance > ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE) { + handleInvite(callInvite, notificationId); + return; } + +// // app is NOT VISIBLE +// if (appImportance > ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE) { +// Intent launchIntent = callNotificationManager.getLaunchIntent( +// (ReactApplicationContext)context, +// notificationId, +// callInvite, +// false, +// 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); +// 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(); +// } +// } } }); } @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) { + handleCanceledCallInvite(cancelledCallInvite); +// Handler handler = new Handler(Looper.getMainLooper()); +// handler.post(new Runnable() { +// public void run() { +// VoiceFirebaseMessagingService.this.sendCancelledCallInviteToActivity(cancelledCallInvite, callException); +// } +// }); } }); @@ -155,13 +165,33 @@ 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); + private void handleInvite(CallInvite callInvite, int notificationId) { + Intent intent = new Intent(this, IncomingCallNotificationService.class); + intent.setAction(ACTION_INCOMING_CALL); + intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId); + intent.putExtra(INCOMING_CALL_INVITE, callInvite); + + startService(intent); + } + + private void handleCanceledCallInvite(CancelledCallInvite cancelledCallInvite) { + Intent intent = new Intent(this, IncomingCallNotificationService.class); + intent.setAction(ACTION_CANCEL_CALL); intent.putExtra(CANCELLED_CALL_INVITE, cancelledCallInvite); - LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + + startService(intent); } + +// /* +// * Send the CancelledCallInvite to the TwilioVoiceModule +// */ +// private void sendCancelledCallInviteToActivity(@NonNull CancelledCallInvite cancelledCallInvite, @Nullable CallException callException) { +// SoundPoolManager.getInstance((this)).stopRinging(); +// Intent intent = new Intent(ACTION_CANCEL_CALL_INVITE); +// intent.putExtra(CANCELLED_CALL_INVITE, cancelledCallInvite); +// if (callException != null) { +// intent.putExtra(CANCELLED_CALL_INVITE_ERR, callException.toString()); +// } +// LocalBroadcastManager.getInstance(this).sendBroadcast(intent); +// } } diff --git a/android/src/main/res/values/values.xml b/android/src/main/res/values/values.xml new file mode 100644 index 00000000..1ac58f4b --- /dev/null +++ b/android/src/main/res/values/values.xml @@ -0,0 +1,5 @@ + + + Answer + Decline + \ No newline at end of file