Skip to content

Commit d115aa4

Browse files
feat: Android Twilio SDK 5.0.1
- notification for incoming call when the app is in the background
1 parent 64c59fa commit d115aa4

9 files changed

+742
-404
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This is a React-Native wrapper for [Twilio Programmable Voice SDK](https://www.t
44

55
## Twilio Programmable Voice SDK
66

7-
- Android 4.5.0 (bundled within the module)
7+
- Android 5.0.0 (bundled within the module)
88
- iOS 5.1.0 (specified by the app's own podfile)
99

1010
## Breaking changes in v4.0.0

android/build.gradle

+2-1
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,10 @@ dependencies {
5555
def supportLibVersion = rootProject.hasProperty('supportLibVersion') ? rootProject.supportLibVersion : DEFAULT_SUPPORT_LIB_VERSION
5656

5757
implementation fileTree(include: ['*.jar'], dir: 'libs')
58-
implementation 'com.twilio:voice-android:4.5.0'
58+
implementation 'com.twilio:voice-android:5.0.1'
5959
implementation "com.android.support:appcompat-v7:$supportLibVersion"
6060
implementation 'com.facebook.react:react-native:+'
6161
implementation 'com.google.firebase:firebase-messaging:17.6.+'
62+
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
6263
testImplementation 'junit:junit:4.12'
6364
}

android/gradle.properties

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,4 @@ org.gradle.jvmargs=-Xmx1536m
1616
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
1717
# org.gradle.parallel=true
1818
android.useAndroidX=true
19-
android.enableJetifier=true
19+
android.enableJetifier=true

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

+125-121
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.hoxfon.react.RNTwilioVoice;
2+
3+
public class Constants {
4+
public static final String INCOMING_NOTIFICATION_PREFIX = "Incoming_";
5+
public static final String MISSED_CALLS_GROUP = "MISSED_CALLS";
6+
public static final int MISSED_CALLS_NOTIFICATION_ID = 1;
7+
public static final int HANGUP_NOTIFICATION_ID = 11;
8+
public static final int CLEAR_MISSED_CALLS_NOTIFICATION_ID = 21;
9+
public static final String PREFERENCE_KEY = "com.hoxfon.react.RNTwilioVoice.PREFERENCE_FILE_KEY";
10+
11+
public static final String CALL_SID_KEY = "CALL_SID";
12+
public static final String VOICE_CHANNEL_LOW_IMPORTANCE = "notification-channel-low-importance";
13+
public static final String VOICE_CHANNEL_HIGH_IMPORTANCE = "notification-channel-high-importance";
14+
public static final String INCOMING_CALL_INVITE = "INCOMING_CALL_INVITE";
15+
public static final String CANCELLED_CALL_INVITE = "CANCELLED_CALL_INVITE";
16+
public static final String INCOMING_CALL_NOTIFICATION_ID = "INCOMING_CALL_NOTIFICATION_ID";
17+
public static final String ACTION_ACCEPT = "com.hoxfon.react.RNTwilioVoice.ACTION_ACCEPT";
18+
public static final String ACTION_REJECT = "com.hoxfon.react.RNTwilioVoice.ACTION_REJECT";
19+
public static final String ACTION_MISSED_CALL = "MISSED_CALL";
20+
public static final String ACTION_ANSWER_CALL = "ANSWER_CALL";
21+
public static final String ACTION_REJECT_CALL = "REJECT_CALL";
22+
public static final String ACTION_HANGUP_CALL = "HANGUP_CALL";
23+
public static final String ACTION_INCOMING_CALL_NOTIFICATION = "ACTION_INCOMING_CALL_NOTIFICATION";
24+
public static final String ACTION_INCOMING_CALL = "ACTION_INCOMING_CALL";
25+
public static final String ACTION_CANCEL_CALL = "ACTION_CANCEL_CALL";
26+
public static final String ACTION_FCM_TOKEN = "ACTION_FCM_TOKEN";
27+
public static final String ACTION_CANCEL_CALL_INVITE = "CANCEL_CALL_INVITE";
28+
public static final String ACTION_CLEAR_MISSED_CALLS_COUNT = "CLEAR_MISSED_CALLS_COUNT";
29+
30+
public static final String NOTIFICATION_TYPE = "NOTIFICATION_TYPE";
31+
public static final String CANCELLED_CALL_INVITE_ERR = "CANCELLED_CALL_INVITE_EXCEPTION";
32+
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package com.hoxfon.react.RNTwilioVoice;
2+
3+
import android.annotation.TargetApi;
4+
import android.app.Notification;
5+
import android.app.NotificationChannel;
6+
import android.app.NotificationManager;
7+
import android.app.PendingIntent;
8+
import android.app.Service;
9+
import android.content.Context;
10+
import android.content.Intent;
11+
import android.graphics.Color;
12+
import android.os.Build;
13+
import android.os.Bundle;
14+
import android.os.IBinder;
15+
import android.util.Log;
16+
17+
import androidx.core.app.NotificationCompat;
18+
import androidx.lifecycle.Lifecycle;
19+
import androidx.lifecycle.ProcessLifecycleOwner;
20+
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
21+
22+
import com.facebook.react.bridge.Arguments;
23+
import com.facebook.react.bridge.ReactApplicationContext;
24+
import com.facebook.react.bridge.WritableMap;
25+
import com.twilio.voice.CallInvite;
26+
27+
import static com.hoxfon.react.RNTwilioVoice.CallNotificationManager.getMainActivityClass;
28+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_ACCEPT;
29+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_CANCEL_CALL;
30+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_INCOMING_CALL;
31+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_INCOMING_CALL_NOTIFICATION;
32+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_REJECT;
33+
import static com.hoxfon.react.RNTwilioVoice.Constants.CALL_SID_KEY;
34+
import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_CALL_INVITE;
35+
import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_CALL_NOTIFICATION_ID;
36+
import static com.hoxfon.react.RNTwilioVoice.EventManager.EVENT_CONNECTION_DID_DISCONNECT;
37+
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG;
38+
39+
public class IncomingCallNotificationService extends Service {
40+
41+
@Override
42+
public int onStartCommand(Intent intent, int flags, int startId) {
43+
String action = intent.getAction();
44+
45+
CallInvite callInvite = intent.getParcelableExtra(INCOMING_CALL_INVITE);
46+
int notificationId = intent.getIntExtra(INCOMING_CALL_NOTIFICATION_ID, 0);
47+
48+
switch (action) {
49+
case ACTION_INCOMING_CALL:
50+
handleIncomingCall(callInvite, notificationId);
51+
break;
52+
case ACTION_ACCEPT:
53+
accept(callInvite, notificationId);
54+
break;
55+
case ACTION_REJECT:
56+
reject(callInvite);
57+
break;
58+
case ACTION_CANCEL_CALL:
59+
handleCancelledCall(intent);
60+
break;
61+
default:
62+
break;
63+
}
64+
return START_NOT_STICKY;
65+
}
66+
67+
@Override
68+
public IBinder onBind(Intent intent) {
69+
return null;
70+
}
71+
72+
private Notification createNotification(CallInvite callInvite, int notificationId, int channelImportance) {
73+
Intent intent = new Intent(this, getMainActivityClass(this));
74+
intent.setAction(ACTION_INCOMING_CALL_NOTIFICATION);
75+
intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
76+
intent.putExtra(INCOMING_CALL_INVITE, callInvite);
77+
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
78+
PendingIntent pendingIntent =
79+
PendingIntent.getActivity(this, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
80+
/*
81+
* Pass the notification id and call sid to use as an identifier to cancel the
82+
* notification later
83+
*/
84+
Bundle extras = new Bundle();
85+
extras.putString(CALL_SID_KEY, callInvite.getCallSid());
86+
87+
String contextText = callInvite.getFrom() + " is calling.";
88+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
89+
// TODO make text configurable from app resources
90+
return buildNotification(contextText,
91+
pendingIntent,
92+
extras,
93+
callInvite,
94+
notificationId,
95+
createChannel(channelImportance));
96+
} else {
97+
// TODO make text configurable from app resources
98+
return new NotificationCompat.Builder(this)
99+
.setSmallIcon(R.drawable.ic_call_white_24dp)
100+
.setContentTitle("Incoming call")
101+
.setContentText(contextText)
102+
.setAutoCancel(true)
103+
.setExtras(extras)
104+
.setContentIntent(pendingIntent)
105+
.setGroup("test_app_notification")
106+
.setColor(Color.rgb(214, 10, 37)).build();
107+
}
108+
}
109+
110+
/**
111+
* Build a notification.
112+
*
113+
* @param text the text of the notification
114+
* @param pendingIntent the body, pending intent for the notification
115+
* @param extras extras passed with the notification
116+
* @return the builder
117+
*/
118+
@TargetApi(Build.VERSION_CODES.O)
119+
private Notification buildNotification(String text, PendingIntent pendingIntent, Bundle extras,
120+
final CallInvite callInvite,
121+
int notificationId,
122+
String channelId) {
123+
Intent rejectIntent = new Intent(getApplicationContext(), IncomingCallNotificationService.class);
124+
rejectIntent.setAction(ACTION_REJECT);
125+
rejectIntent.putExtra(INCOMING_CALL_INVITE, callInvite);
126+
rejectIntent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
127+
PendingIntent piRejectIntent = PendingIntent.getService(getApplicationContext(), 0, rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT);
128+
129+
Intent acceptIntent = new Intent(getApplicationContext(), IncomingCallNotificationService.class);
130+
acceptIntent.setAction(ACTION_ACCEPT);
131+
acceptIntent.putExtra(INCOMING_CALL_INVITE, callInvite);
132+
acceptIntent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
133+
PendingIntent piAcceptIntent = PendingIntent.getService(getApplicationContext(), 0, acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT);
134+
135+
Notification.Builder builder =
136+
new Notification.Builder(getApplicationContext(), channelId)
137+
.setSmallIcon(R.drawable.ic_call_white_24dp)
138+
.setContentTitle("Incoming call")
139+
.setContentText(text)
140+
.setCategory(Notification.CATEGORY_CALL)
141+
.setExtras(extras)
142+
.setAutoCancel(true)
143+
.addAction(android.R.drawable.ic_menu_delete, getString(R.string.decline), piRejectIntent)
144+
.addAction(android.R.drawable.ic_menu_call, getString(R.string.answer), piAcceptIntent)
145+
.setFullScreenIntent(pendingIntent, true);
146+
147+
return builder.build();
148+
}
149+
150+
@TargetApi(Build.VERSION_CODES.O)
151+
private String createChannel(int channelImportance) {
152+
NotificationChannel callInviteChannel = new NotificationChannel(Constants.VOICE_CHANNEL_HIGH_IMPORTANCE,
153+
"Primary Voice Channel", NotificationManager.IMPORTANCE_HIGH);;
154+
String channelId = Constants.VOICE_CHANNEL_HIGH_IMPORTANCE;
155+
156+
if (channelImportance == NotificationManager.IMPORTANCE_LOW) {
157+
callInviteChannel = new NotificationChannel(Constants.VOICE_CHANNEL_LOW_IMPORTANCE,
158+
"Primary Voice Channel", NotificationManager.IMPORTANCE_LOW);;
159+
channelId = Constants.VOICE_CHANNEL_LOW_IMPORTANCE;
160+
}
161+
callInviteChannel.setLightColor(Color.GREEN);
162+
callInviteChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
163+
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
164+
notificationManager.createNotificationChannel(callInviteChannel);
165+
166+
return channelId;
167+
}
168+
169+
private void accept(CallInvite callInvite, int notificationId) {
170+
endForeground();
171+
Intent activeCallIntent = new Intent(this, getMainActivityClass(this));
172+
// Intent activeCallIntent = new Intent(this, TwilioVoiceModule.class);
173+
activeCallIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
174+
activeCallIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
175+
activeCallIntent.putExtra(INCOMING_CALL_INVITE, callInvite);
176+
activeCallIntent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
177+
activeCallIntent.setAction(ACTION_ACCEPT);
178+
startActivity(activeCallIntent);
179+
}
180+
181+
private void reject(CallInvite callInvite) {
182+
endForeground();
183+
callInvite.reject(getApplicationContext());
184+
}
185+
186+
private void handleCancelledCall(Intent intent) {
187+
endForeground();
188+
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
189+
}
190+
191+
private void handleIncomingCall(CallInvite callInvite, int notificationId) {
192+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
193+
setCallInProgressNotification(callInvite, notificationId);
194+
}
195+
sendCallInviteToActivity(callInvite, notificationId);
196+
}
197+
198+
private void endForeground() {
199+
stopForeground(true);
200+
}
201+
202+
private void setCallInProgressNotification(CallInvite callInvite, int notificationId) {
203+
int importance = NotificationManager.IMPORTANCE_LOW;
204+
if (!isAppVisible()) {
205+
Log.i(TAG, "setCallInProgressNotification - app is NOT visible.");
206+
importance = NotificationManager.IMPORTANCE_HIGH;
207+
}
208+
startForeground(notificationId, createNotification(callInvite, notificationId, importance));
209+
}
210+
211+
/*
212+
* Send the CallInvite to the Activity. Start the activity if it is not running already.
213+
*/
214+
private void sendCallInviteToActivity(CallInvite callInvite, int notificationId) {
215+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !isAppVisible()) {
216+
return;
217+
}
218+
Intent intent = new Intent(this, getMainActivityClass(this));
219+
intent.setAction(ACTION_INCOMING_CALL);
220+
intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
221+
intent.putExtra(INCOMING_CALL_INVITE, callInvite);
222+
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
223+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
224+
this.startActivity(intent);
225+
}
226+
227+
private boolean isAppVisible() {
228+
return ProcessLifecycleOwner
229+
.get()
230+
.getLifecycle()
231+
.getCurrentState()
232+
.isAtLeast(Lifecycle.State.STARTED);
233+
}
234+
}

0 commit comments

Comments
 (0)