Skip to content

Commit

Permalink
fix: incoming call when the app is killed
Browse files Browse the repository at this point in the history
  • Loading branch information
fabriziomoscon committed Jan 2, 2021
1 parent ece4201 commit 9a8f98f
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 67 deletions.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,56 @@ To allow the library to show heads up notifications you must add the following l
</application>
```

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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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";
Expand All @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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
Expand Down Expand Up @@ -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.
*
Expand All @@ -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)
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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());
}
Expand All @@ -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;
}
Expand All @@ -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);
}

Expand All @@ -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());
}
Expand All @@ -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()));
Expand All @@ -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());
}
Expand All @@ -355,20 +347,19 @@ 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;
}
eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params);
callNotificationManager.removeHangupNotification(getReactApplicationContext());
toNumber = "";
toName = "";
activeCallInvite = null;
}
};
}
Expand Down Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 9a8f98f

Please sign in to comment.