From b50f2be430fed8a51363e5d97f6fea5f986891e7 Mon Sep 17 00:00:00 2001 From: Fabrizio Moscon Date: Sat, 2 Jan 2021 19:17:44 +0000 Subject: [PATCH] fix: incoming call when the app is killed --- README.md | 50 +++++++++++ .../hoxfon/react/RNTwilioVoice/Constants.java | 6 ++ .../IncomingCallNotificationService.java | 72 +++++++++++++--- .../RNTwilioVoice/TwilioVoiceModule.java | 83 ++++++++----------- android/src/main/res/values/style.xml | 5 ++ 5 files changed, 153 insertions(+), 63 deletions(-) create mode 100644 android/src/main/res/values/style.xml diff --git a/README.md b/README.md index 294525aa..b5e025f7 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,56 @@ To allow the library to show heads up notifications you must add the following l ``` +Launch your app with `callInvite` or `call` initial properties. +Add the following lines to your app `MainActivity`: + +```java + +import com.hoxfon.react.RNTwilioVoice.Constants; +... + +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() { + Bundle initialProperties = new Bundle(); + Intent intent = this.getPlainActivity().getIntent(); + if (intent == null) { + return initialProperties; + } + switch (intent.getAction()) { + case Constants.ACTION_INCOMING_CALL_NOTIFICATION: + Bundle callInviteBundle = new Bundle(); + callInviteBundle.putString(Constants.CALL_SID, intent.getStringExtra(Constants.CALL_SID)); + callInviteBundle.putString(Constants.CALL_FROM, intent.getStringExtra(Constants.CALL_FROM)); + callInviteBundle.putString(Constants.CALL_TO, intent.getStringExtra(Constants.CALL_TO)); + initialProperties.putBundle(Constants.CALL_INVITE_KEY, callInviteBundle); + break; + + case Constants.ACTION_ACCEPT: + Bundle callBundle = new Bundle(); + 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; + } + }; + } + ... +} +``` + ## ICE See https://www.twilio.com/docs/stun-turn diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/Constants.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/Constants.java index be310372..388c00e8 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/Constants.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/Constants.java @@ -1,5 +1,7 @@ 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; @@ -19,6 +21,7 @@ public class Constants { 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 = "com.hoxfon.react.RNTwilioVoice.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_CLEAR_MISSED_CALLS_COUNT = "CLEAR_MISSED_CALLS_COUNT"; @@ -29,4 +32,7 @@ public class Constants { 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(); } \ No newline at end of file diff --git a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java index 05741a8c..37dcbb81 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java @@ -18,8 +18,13 @@ 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; @@ -47,15 +52,19 @@ public int onStartCommand(Intent intent, int flags, int startId) { 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; + default: break; } @@ -70,14 +79,17 @@ public IBinder onBind(Intent intent) { private Notification createNotification(CallInvite callInvite, int notificationId, int channelImportance) { Context context = getApplicationContext(); - Intent intent = new Intent(context, getMainActivityClass(context)); - intent.setAction(Constants.ACTION_INCOMING_CALL); + 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); PendingIntent pendingIntent = - PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); + PendingIntent.getActivity(this, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT); /* * Pass the notification id and call sid to use as an identifier to cancel the @@ -109,6 +121,28 @@ private Notification createNotification(CallInvite callInvite, int notificationI } } + 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. * @@ -128,15 +162,21 @@ private Notification buildNotification(String text, PendingIntent pendingIntent, rejectIntent.setAction(Constants.ACTION_REJECT); rejectIntent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite); rejectIntent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId); - PendingIntent piRejectIntent = PendingIntent.getService(getApplicationContext(), 0, rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT); - NotificationCompat.Action rejectAction = new NotificationCompat.Action.Builder(android.R.drawable.ic_menu_delete, getString(R.string.reject), piRejectIntent).build(); + 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); - PendingIntent piAcceptIntent = PendingIntent.getService(getApplicationContext(), 0, acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT); - NotificationCompat.Action answerAction = new NotificationCompat.Action.Builder(android.R.drawable.ic_menu_call, getString(R.string.accept), piAcceptIntent).build(); + 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) @@ -197,6 +237,9 @@ private void accept(CallInvite callInvite, int notificationId) { 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); } @@ -241,19 +284,22 @@ private void setCallInProgressNotification(CallInvite callInvite, int notificati * Send the CallInvite to the Activity. Start the activity if it is not running already. */ private void sendCallInviteToActivity(CallInvite callInvite, int notificationId) { - // TODO in case the app is killed there is not enough time for the incoming call event to be sent to JS - // therefore leaving the heads up notification present is the only way to allow the call to be answered - // endForeground(); if (BuildConfig.DEBUG) { - Log.d(TAG, "sendCallInviteToActivity()"); + Log.d(TAG, "sendCallInviteToActivity(). Android SDK: " + Build.VERSION.SDK_INT + " app visible: " + isAppVisible()); } - SoundPoolManager.getInstance(this).playRinging(); - // From Android 29 app are prevented to start an activity from the background + // 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); 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 033d820e..fe2afa5e 100644 --- a/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java +++ b/android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java @@ -230,7 +230,7 @@ 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.Listener().onRinging(). Call state: " + call.getState() + ". Call: "+ call.toString()); @@ -244,7 +244,7 @@ public void onRinging(Call call) { } @Override - public void onConnected(Call call) { + public void onConnected(@NonNull Call call) { if (BuildConfig.DEBUG) { Log.d(TAG, "Call.Listener().onConnected(). Call state: " + call.getState()); } @@ -253,21 +253,19 @@ public void onConnected(Call call) { headsetManager.startWiredHeadsetEvent(getReactApplicationContext()); WritableMap params = Arguments.createMap(); - if (call != null) { - 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); + 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; } @@ -283,11 +281,9 @@ public void onReconnecting(@NonNull Call call, @NonNull CallException callExcept Log.d(TAG, "Call.Listener().onReconnecting(). Call state: " + call.getState()); } WritableMap params = Arguments.createMap(); - if (call != null) { - params.putString(Constants.CALL_SID, call.getSid()); - params.putString(Constants.CALL_FROM, call.getFrom()); - params.putString(Constants.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); } @@ -300,16 +296,14 @@ public void onReconnected(@NonNull Call call) { Log.d(TAG, "Call.Listener().onReconnected(). Call state: " + call.getState()); } WritableMap params = Arguments.createMap(); - if (call != null) { - params.putString(Constants.CALL_SID, call.getSid()); - params.putString(Constants.CALL_FROM, call.getFrom()); - params.putString(Constants.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.Listener().onDisconnected(). Call state: " + call.getState()); } @@ -319,13 +313,11 @@ public void onDisconnected(Call call, CallException error) { WritableMap params = Arguments.createMap(); String callSid = ""; - if (call != null) { - 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()); - } + 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())); @@ -342,7 +334,7 @@ public void onDisconnected(Call call, CallException error) { } @Override - public void onConnectFailure(Call call, CallException error) { + public void onConnectFailure(@NonNull Call call, CallException error) { if (BuildConfig.DEBUG) { Log.d(TAG, "Call.Listener().onConnectFailure(). Call state: " + call.getState()); } @@ -355,13 +347,11 @@ public void onConnectFailure(Call call, CallException error) { WritableMap params = Arguments.createMap(); params.putString(Constants.ERROR, error.getMessage()); String callSid = ""; - if (call != null) { - 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()); - } + 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; } @@ -369,6 +359,7 @@ public void onConnectFailure(Call call, CallException error) { callNotificationManager.removeHangupNotification(getReactApplicationContext()); toNumber = ""; toName = ""; + activeCallInvite = null; } }; } @@ -500,14 +491,6 @@ private void handleStartActivityIntent(Intent intent) { acceptFromIntent(intent); break; - case Constants.ACTION_REJECT: - reject(); - break; - - case Constants.ACTION_INCOMING_CALL: - handleCallInviteNotification(); - break; - case Constants.ACTION_OPEN_CALL_IN_PROGRESS: // the notification already brings the activity to the top break; 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