Skip to content

Commit b50f2be

Browse files
fix: incoming call when the app is killed
1 parent ece4201 commit b50f2be

File tree

5 files changed

+153
-63
lines changed

5 files changed

+153
-63
lines changed

README.md

+50
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,56 @@ To allow the library to show heads up notifications you must add the following l
3838
</application>
3939
```
4040

41+
Launch your app with `callInvite` or `call` initial properties.
42+
Add the following lines to your app `MainActivity`:
43+
44+
```java
45+
46+
import com.hoxfon.react.RNTwilioVoice.Constants;
47+
...
48+
49+
public class MainActivity extends ReactActivity {
50+
51+
@Override
52+
protected ReactActivityDelegate createReactActivityDelegate() {
53+
return new ReactActivityDelegate(this, getMainComponentName()) {
54+
@Override
55+
protected ReactRootView createRootView() {
56+
return new RNGestureHandlerEnabledRootView(MainActivity.this);
57+
}
58+
@Override
59+
protected Bundle getLaunchOptions() {
60+
Bundle initialProperties = new Bundle();
61+
Intent intent = this.getPlainActivity().getIntent();
62+
if (intent == null) {
63+
return initialProperties;
64+
}
65+
switch (intent.getAction()) {
66+
case Constants.ACTION_INCOMING_CALL_NOTIFICATION:
67+
Bundle callInviteBundle = new Bundle();
68+
callInviteBundle.putString(Constants.CALL_SID, intent.getStringExtra(Constants.CALL_SID));
69+
callInviteBundle.putString(Constants.CALL_FROM, intent.getStringExtra(Constants.CALL_FROM));
70+
callInviteBundle.putString(Constants.CALL_TO, intent.getStringExtra(Constants.CALL_TO));
71+
initialProperties.putBundle(Constants.CALL_INVITE_KEY, callInviteBundle);
72+
break;
73+
74+
case Constants.ACTION_ACCEPT:
75+
Bundle callBundle = new Bundle();
76+
callBundle.putString(Constants.CALL_SID, intent.getStringExtra(Constants.CALL_SID));
77+
callBundle.putString(Constants.CALL_FROM, intent.getStringExtra(Constants.CALL_FROM));
78+
callBundle.putString(Constants.CALL_TO, intent.getStringExtra(Constants.CALL_TO));
79+
callBundle.putString(Constants.CALL_STATE, Constants.CALL_STATE_CONNECTED);
80+
initialProperties.putBundle(Constants.CALL_KEY, callBundle);
81+
break;
82+
}
83+
return initialProperties;
84+
}
85+
};
86+
}
87+
...
88+
}
89+
```
90+
4191
## ICE
4292

4393
See https://www.twilio.com/docs/stun-turn

android/src/main/java/com/hoxfon/react/RNTwilioVoice/Constants.java

+6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.hoxfon.react.RNTwilioVoice;
22

3+
import com.twilio.voice.Call;
4+
35
public class Constants {
46
public static final String MISSED_CALLS_GROUP = "MISSED_CALLS";
57
public static final int MISSED_CALLS_NOTIFICATION_ID = 1;
@@ -19,6 +21,7 @@ public class Constants {
1921
public static final String ACTION_MISSED_CALL = "MISSED_CALL";
2022
public static final String ACTION_HANGUP_CALL = "HANGUP_CALL";
2123
public static final String ACTION_INCOMING_CALL = "ACTION_INCOMING_CALL";
24+
public static final String ACTION_INCOMING_CALL_NOTIFICATION = "com.hoxfon.react.RNTwilioVoice.ACTION_INCOMING_CALL";
2225
public static final String ACTION_CANCEL_CALL = "ACTION_CANCEL_CALL";
2326
public static final String ACTION_FCM_TOKEN = "ACTION_FCM_TOKEN";
2427
public static final String ACTION_CLEAR_MISSED_CALLS_COUNT = "CLEAR_MISSED_CALLS_COUNT";
@@ -29,4 +32,7 @@ public class Constants {
2932
public static final String CALL_FROM = "call_from";
3033
public static final String CALL_TO = "call_to";
3134
public static final String ERROR = "err";
35+
public static final String CALL_KEY = "call";
36+
public static final String CALL_INVITE_KEY = "callInvite";
37+
public static final String CALL_STATE_CONNECTED = Call.State.CONNECTED.toString();
3238
}

android/src/main/java/com/hoxfon/react/RNTwilioVoice/IncomingCallNotificationService.java

+59-13
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@
1818
import android.os.Build;
1919
import android.os.Bundle;
2020
import android.os.IBinder;
21+
import android.text.Spannable;
22+
import android.text.SpannableString;
23+
import android.text.style.ForegroundColorSpan;
2124
import android.util.Log;
2225

26+
import androidx.annotation.ColorRes;
27+
import androidx.annotation.StringRes;
2328
import androidx.core.app.NotificationCompat;
2429
import androidx.lifecycle.Lifecycle;
2530
import androidx.lifecycle.ProcessLifecycleOwner;
@@ -47,15 +52,19 @@ public int onStartCommand(Intent intent, int flags, int startId) {
4752
case Constants.ACTION_INCOMING_CALL:
4853
handleIncomingCall(callInvite, notificationId);
4954
break;
55+
5056
case Constants.ACTION_ACCEPT:
5157
accept(callInvite, notificationId);
5258
break;
59+
5360
case Constants.ACTION_REJECT:
5461
reject(callInvite, notificationId);
5562
break;
63+
5664
case Constants.ACTION_CANCEL_CALL:
5765
handleCancelledCall(intent);
5866
break;
67+
5968
default:
6069
break;
6170
}
@@ -70,14 +79,17 @@ public IBinder onBind(Intent intent) {
7079
private Notification createNotification(CallInvite callInvite, int notificationId, int channelImportance) {
7180
Context context = getApplicationContext();
7281

73-
Intent intent = new Intent(context, getMainActivityClass(context));
74-
intent.setAction(Constants.ACTION_INCOMING_CALL);
82+
Intent intent = new Intent(this, getMainActivityClass(context));
83+
intent.setAction(Constants.ACTION_INCOMING_CALL_NOTIFICATION);
7584
intent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);
7685
intent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite);
86+
intent.putExtra(Constants.CALL_SID, callInvite.getCallSid());
87+
intent.putExtra(Constants.CALL_FROM, callInvite.getFrom());
88+
intent.putExtra(Constants.CALL_TO, callInvite.getTo());
7789
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
7890

7991
PendingIntent pendingIntent =
80-
PendingIntent.getActivity(context, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
92+
PendingIntent.getActivity(this, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
8193

8294
/*
8395
* 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
109121
}
110122
}
111123

124+
private Spannable getActionText(Context context, @StringRes int stringRes, @ColorRes int colorRes) {
125+
Spannable spannable = new SpannableString(context.getText(stringRes));
126+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
127+
spannable.setSpan(
128+
new ForegroundColorSpan(context.getColor(colorRes)),
129+
0,
130+
spannable.length(),
131+
0
132+
);
133+
}
134+
return spannable;
135+
}
136+
137+
private PendingIntent createActionPendingIntent(Context context, Intent intent) {
138+
return PendingIntent.getService(
139+
context,
140+
0,
141+
intent,
142+
PendingIntent.FLAG_UPDATE_CURRENT
143+
);
144+
}
145+
112146
/**
113147
* Build a notification.
114148
*
@@ -128,15 +162,21 @@ private Notification buildNotification(String text, PendingIntent pendingIntent,
128162
rejectIntent.setAction(Constants.ACTION_REJECT);
129163
rejectIntent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite);
130164
rejectIntent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);
131-
PendingIntent piRejectIntent = PendingIntent.getService(getApplicationContext(), 0, rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT);
132-
NotificationCompat.Action rejectAction = new NotificationCompat.Action.Builder(android.R.drawable.ic_menu_delete, getString(R.string.reject), piRejectIntent).build();
165+
NotificationCompat.Action rejectAction = new NotificationCompat.Action.Builder(
166+
android.R.drawable.ic_menu_delete,
167+
getActionText(context, R.string.reject, R.color.red),
168+
createActionPendingIntent(context, rejectIntent)
169+
).build();
133170

134171
Intent acceptIntent = new Intent(context, IncomingCallNotificationService.class);
135172
acceptIntent.setAction(Constants.ACTION_ACCEPT);
136173
acceptIntent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite);
137174
acceptIntent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);
138-
PendingIntent piAcceptIntent = PendingIntent.getService(getApplicationContext(), 0, acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT);
139-
NotificationCompat.Action answerAction = new NotificationCompat.Action.Builder(android.R.drawable.ic_menu_call, getString(R.string.accept), piAcceptIntent).build();
175+
NotificationCompat.Action answerAction = new NotificationCompat.Action.Builder(
176+
android.R.drawable.ic_menu_call,
177+
getActionText(context, R.string.accept, R.color.green),
178+
createActionPendingIntent(context, acceptIntent)
179+
).build();
140180

141181
NotificationCompat.Builder builder =
142182
new NotificationCompat.Builder(context, channelId)
@@ -197,6 +237,9 @@ private void accept(CallInvite callInvite, int notificationId) {
197237
activeCallIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
198238
activeCallIntent.putExtra(Constants.INCOMING_CALL_INVITE, callInvite);
199239
activeCallIntent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);
240+
activeCallIntent.putExtra(Constants.CALL_SID, callInvite.getCallSid());
241+
activeCallIntent.putExtra(Constants.CALL_FROM, callInvite.getFrom());
242+
activeCallIntent.putExtra(Constants.CALL_TO, callInvite.getTo());
200243
activeCallIntent.setAction(Constants.ACTION_ACCEPT);
201244
this.startActivity(activeCallIntent);
202245
}
@@ -241,19 +284,22 @@ private void setCallInProgressNotification(CallInvite callInvite, int notificati
241284
* Send the CallInvite to the Activity. Start the activity if it is not running already.
242285
*/
243286
private void sendCallInviteToActivity(CallInvite callInvite, int notificationId) {
244-
// TODO in case the app is killed there is not enough time for the incoming call event to be sent to JS
245-
// therefore leaving the heads up notification present is the only way to allow the call to be answered
246-
// endForeground();
247287
if (BuildConfig.DEBUG) {
248-
Log.d(TAG, "sendCallInviteToActivity()");
288+
Log.d(TAG, "sendCallInviteToActivity(). Android SDK: " + Build.VERSION.SDK_INT + " app visible: " + isAppVisible());
249289
}
250-
251290
SoundPoolManager.getInstance(this).playRinging();
252291

253-
// From Android 29 app are prevented to start an activity from the background
292+
// From Android SDK 29 apps are prevented to start an activity from the background
254293
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !isAppVisible()) {
294+
if (BuildConfig.DEBUG) {
295+
Log.d(TAG, "sendCallInviteToActivity(). DO NOTHING");
296+
}
255297
return;
256298
}
299+
if (BuildConfig.DEBUG) {
300+
Log.d(TAG, "sendCallInviteToActivity(). startActivity()");
301+
}
302+
// Android SDK < 29 or app is visible
257303
Intent intent = new Intent(this, getMainActivityClass(this));
258304
intent.setAction(Constants.ACTION_INCOMING_CALL);
259305
intent.putExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, notificationId);

android/src/main/java/com/hoxfon/react/RNTwilioVoice/TwilioVoiceModule.java

+33-50
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ private Call.Listener callListener() {
230230
* raised, irrespective of the value of answerOnBridge being set to true or false
231231
*/
232232
@Override
233-
public void onRinging(Call call) {
233+
public void onRinging(@NonNull Call call) {
234234
// TODO test this with JS app
235235
if (BuildConfig.DEBUG) {
236236
Log.d(TAG, "Call.Listener().onRinging(). Call state: " + call.getState() + ". Call: "+ call.toString());
@@ -244,7 +244,7 @@ public void onRinging(Call call) {
244244
}
245245

246246
@Override
247-
public void onConnected(Call call) {
247+
public void onConnected(@NonNull Call call) {
248248
if (BuildConfig.DEBUG) {
249249
Log.d(TAG, "Call.Listener().onConnected(). Call state: " + call.getState());
250250
}
@@ -253,21 +253,19 @@ public void onConnected(Call call) {
253253
headsetManager.startWiredHeadsetEvent(getReactApplicationContext());
254254

255255
WritableMap params = Arguments.createMap();
256-
if (call != null) {
257-
params.putString(Constants.CALL_SID, call.getSid());
258-
params.putString(Constants.CALL_STATE, call.getState().name());
259-
params.putString(Constants.CALL_FROM, call.getFrom());
260-
params.putString(Constants.CALL_TO, call.getTo());
261-
String caller = "Show call details in the app";
262-
if (!toName.equals("")) {
263-
caller = toName;
264-
} else if (!toNumber.equals("")) {
265-
caller = toNumber;
266-
}
267-
activeCall = call;
268-
callNotificationManager.createHangupNotification(getReactApplicationContext(),
269-
call.getSid(), caller);
256+
params.putString(Constants.CALL_SID, call.getSid());
257+
params.putString(Constants.CALL_STATE, call.getState().name());
258+
params.putString(Constants.CALL_FROM, call.getFrom());
259+
params.putString(Constants.CALL_TO, call.getTo());
260+
String caller = "Show call details in the app";
261+
if (!toName.equals("")) {
262+
caller = toName;
263+
} else if (!toNumber.equals("")) {
264+
caller = toNumber;
270265
}
266+
activeCall = call;
267+
callNotificationManager.createHangupNotification(getReactApplicationContext(),
268+
call.getSid(), caller);
271269
eventManager.sendEvent(EVENT_CONNECTION_DID_CONNECT, params);
272270
activeCallInvite = null;
273271
}
@@ -283,11 +281,9 @@ public void onReconnecting(@NonNull Call call, @NonNull CallException callExcept
283281
Log.d(TAG, "Call.Listener().onReconnecting(). Call state: " + call.getState());
284282
}
285283
WritableMap params = Arguments.createMap();
286-
if (call != null) {
287-
params.putString(Constants.CALL_SID, call.getSid());
288-
params.putString(Constants.CALL_FROM, call.getFrom());
289-
params.putString(Constants.CALL_TO, call.getTo());
290-
}
284+
params.putString(Constants.CALL_SID, call.getSid());
285+
params.putString(Constants.CALL_FROM, call.getFrom());
286+
params.putString(Constants.CALL_TO, call.getTo());
291287
eventManager.sendEvent(EVENT_CONNECTION_IS_RECONNECTING, params);
292288
}
293289

@@ -300,16 +296,14 @@ public void onReconnected(@NonNull Call call) {
300296
Log.d(TAG, "Call.Listener().onReconnected(). Call state: " + call.getState());
301297
}
302298
WritableMap params = Arguments.createMap();
303-
if (call != null) {
304-
params.putString(Constants.CALL_SID, call.getSid());
305-
params.putString(Constants.CALL_FROM, call.getFrom());
306-
params.putString(Constants.CALL_TO, call.getTo());
307-
}
299+
params.putString(Constants.CALL_SID, call.getSid());
300+
params.putString(Constants.CALL_FROM, call.getFrom());
301+
params.putString(Constants.CALL_TO, call.getTo());
308302
eventManager.sendEvent(EVENT_CONNECTION_DID_RECONNECT, params);
309303
}
310304

311305
@Override
312-
public void onDisconnected(Call call, CallException error) {
306+
public void onDisconnected(@NonNull Call call, CallException error) {
313307
if (BuildConfig.DEBUG) {
314308
Log.d(TAG, "Call.Listener().onDisconnected(). Call state: " + call.getState());
315309
}
@@ -319,13 +313,11 @@ public void onDisconnected(Call call, CallException error) {
319313

320314
WritableMap params = Arguments.createMap();
321315
String callSid = "";
322-
if (call != null) {
323-
callSid = call.getSid();
324-
params.putString(Constants.CALL_SID, callSid);
325-
params.putString(Constants.CALL_STATE, call.getState().name());
326-
params.putString(Constants.CALL_FROM, call.getFrom());
327-
params.putString(Constants.CALL_TO, call.getTo());
328-
}
316+
callSid = call.getSid();
317+
params.putString(Constants.CALL_SID, callSid);
318+
params.putString(Constants.CALL_STATE, call.getState().name());
319+
params.putString(Constants.CALL_FROM, call.getFrom());
320+
params.putString(Constants.CALL_TO, call.getTo());
329321
if (error != null) {
330322
Log.e(TAG, String.format("CallListener onDisconnected error: %d, %s",
331323
error.getErrorCode(), error.getMessage()));
@@ -342,7 +334,7 @@ public void onDisconnected(Call call, CallException error) {
342334
}
343335

344336
@Override
345-
public void onConnectFailure(Call call, CallException error) {
337+
public void onConnectFailure(@NonNull Call call, CallException error) {
346338
if (BuildConfig.DEBUG) {
347339
Log.d(TAG, "Call.Listener().onConnectFailure(). Call state: " + call.getState());
348340
}
@@ -355,20 +347,19 @@ public void onConnectFailure(Call call, CallException error) {
355347
WritableMap params = Arguments.createMap();
356348
params.putString(Constants.ERROR, error.getMessage());
357349
String callSid = "";
358-
if (call != null) {
359-
callSid = call.getSid();
360-
params.putString(Constants.CALL_SID, callSid);
361-
params.putString(Constants.CALL_STATE, call.getState().name());
362-
params.putString(Constants.CALL_FROM, call.getFrom());
363-
params.putString(Constants.CALL_TO, call.getTo());
364-
}
350+
callSid = call.getSid();
351+
params.putString(Constants.CALL_SID, callSid);
352+
params.putString(Constants.CALL_STATE, call.getState().name());
353+
params.putString(Constants.CALL_FROM, call.getFrom());
354+
params.putString(Constants.CALL_TO, call.getTo());
365355
if (callSid != null && activeCall != null && activeCall.getSid() != null && activeCall.getSid().equals(callSid)) {
366356
activeCall = null;
367357
}
368358
eventManager.sendEvent(EVENT_CONNECTION_DID_DISCONNECT, params);
369359
callNotificationManager.removeHangupNotification(getReactApplicationContext());
370360
toNumber = "";
371361
toName = "";
362+
activeCallInvite = null;
372363
}
373364
};
374365
}
@@ -500,14 +491,6 @@ private void handleStartActivityIntent(Intent intent) {
500491
acceptFromIntent(intent);
501492
break;
502493

503-
case Constants.ACTION_REJECT:
504-
reject();
505-
break;
506-
507-
case Constants.ACTION_INCOMING_CALL:
508-
handleCallInviteNotification();
509-
break;
510-
511494
case Constants.ACTION_OPEN_CALL_IN_PROGRESS:
512495
// the notification already brings the activity to the top
513496
break;

android/src/main/res/values/style.xml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<color name="red">#d6312e</color>
4+
<color name="green">#258c42</color>
5+
</resources>

0 commit comments

Comments
 (0)