Skip to content

Commit 98c7b0f

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

9 files changed

+333
-78
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

+17-19
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,23 @@
2626
import java.util.List;
2727

2828
import static android.content.Context.ACTIVITY_SERVICE;
29-
29+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_ANSWER_CALL;
30+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_REJECT_CALL;
31+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_HANGUP_CALL;
32+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_INCOMING_CALL;
33+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_MISSED_CALL;
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.Constants.NOTIFICATION_TYPE;
37+
import static com.hoxfon.react.RNTwilioVoice.Constants.CALL_SID_KEY;
38+
import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_NOTIFICATION_PREFIX;
39+
import static com.hoxfon.react.RNTwilioVoice.Constants.MISSED_CALLS_GROUP;
40+
import static com.hoxfon.react.RNTwilioVoice.Constants.MISSED_CALLS_NOTIFICATION_ID;
41+
import static com.hoxfon.react.RNTwilioVoice.Constants.HANGUP_NOTIFICATION_ID;
42+
import static com.hoxfon.react.RNTwilioVoice.Constants.PREFERENCE_KEY;
43+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_CLEAR_MISSED_CALLS_COUNT;
44+
import static com.hoxfon.react.RNTwilioVoice.Constants.CLEAR_MISSED_CALLS_NOTIFICATION_ID;
3045
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG;
31-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_ANSWER_CALL;
32-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_REJECT_CALL;
33-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_HANGUP_CALL;
34-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_INCOMING_CALL;
35-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_MISSED_CALL;
36-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_INVITE;
37-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_NOTIFICATION_ID;
38-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.NOTIFICATION_TYPE;
39-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.CALL_SID_KEY;
40-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_NOTIFICATION_PREFIX;
41-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.MISSED_CALLS_GROUP;
42-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.MISSED_CALLS_NOTIFICATION_ID;
43-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.HANGUP_NOTIFICATION_ID;
44-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.PREFERENCE_KEY;
45-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_CLEAR_MISSED_CALLS_COUNT;
46-
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.CLEAR_MISSED_CALLS_NOTIFICATION_ID;
47-
4846

4947
public class CallNotificationManager {
5048

@@ -72,7 +70,7 @@ public int getApplicationImportance(ReactApplicationContext context) {
7270
return 0;
7371
}
7472

75-
public Class getMainActivityClass(ReactApplicationContext context) {
73+
public static Class getMainActivityClass(Context context) {
7674
String packageName = context.getPackageName();
7775
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
7876
String className = launchIntent.getComponent().getClassName();
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.TwilioVoice.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 = "ACTION_ACCEPT";
18+
public static final String ACTION_REJECT = "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,229 @@
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.twilio.voice.CallInvite;
23+
24+
import static com.hoxfon.react.RNTwilioVoice.CallNotificationManager.getMainActivityClass;
25+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_ACCEPT;
26+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_CANCEL_CALL;
27+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_INCOMING_CALL;
28+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_INCOMING_CALL_NOTIFICATION;
29+
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_REJECT;
30+
import static com.hoxfon.react.RNTwilioVoice.Constants.CALL_SID_KEY;
31+
import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_CALL_INVITE;
32+
import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_CALL_NOTIFICATION_ID;
33+
34+
public class IncomingCallNotificationService extends Service {
35+
36+
private static final String TAG = IncomingCallNotificationService.class.getSimpleName();
37+
38+
@Override
39+
public int onStartCommand(Intent intent, int flags, int startId) {
40+
String action = intent.getAction();
41+
42+
CallInvite callInvite = intent.getParcelableExtra(INCOMING_CALL_INVITE);
43+
int notificationId = intent.getIntExtra(INCOMING_CALL_NOTIFICATION_ID, 0);
44+
45+
switch (action) {
46+
case ACTION_INCOMING_CALL:
47+
handleIncomingCall(callInvite, notificationId);
48+
break;
49+
case ACTION_ACCEPT:
50+
accept(callInvite, notificationId);
51+
break;
52+
case ACTION_REJECT:
53+
reject(callInvite);
54+
break;
55+
case ACTION_CANCEL_CALL:
56+
handleCancelledCall(intent);
57+
break;
58+
default:
59+
break;
60+
}
61+
return START_NOT_STICKY;
62+
}
63+
64+
@Override
65+
public IBinder onBind(Intent intent) {
66+
return null;
67+
}
68+
69+
private Notification createNotification(CallInvite callInvite, int notificationId, int channelImportance) {
70+
Intent intent = new Intent(this, getMainActivityClass(this));
71+
intent.setAction(ACTION_INCOMING_CALL_NOTIFICATION);
72+
intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
73+
intent.putExtra(INCOMING_CALL_INVITE, callInvite);
74+
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
75+
PendingIntent pendingIntent =
76+
PendingIntent.getActivity(this, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
77+
/*
78+
* Pass the notification id and call sid to use as an identifier to cancel the
79+
* notification later
80+
*/
81+
Bundle extras = new Bundle();
82+
extras.putString(CALL_SID_KEY, callInvite.getCallSid());
83+
84+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
85+
return buildNotification(callInvite.getFrom() + " is calling.",
86+
pendingIntent,
87+
extras,
88+
callInvite,
89+
notificationId,
90+
createChannel(channelImportance));
91+
} else {
92+
return new NotificationCompat.Builder(this)
93+
.setSmallIcon(R.drawable.ic_call_white_24dp)
94+
.setContentTitle("Incoming call")
95+
.setContentText(callInvite.getFrom() + " is calling.")
96+
.setAutoCancel(true)
97+
.setExtras(extras)
98+
.setContentIntent(pendingIntent)
99+
.setGroup("test_app_notification")
100+
.setColor(Color.rgb(214, 10, 37)).build();
101+
}
102+
}
103+
104+
/**
105+
* Build a notification.
106+
*
107+
* @param text the text of the notification
108+
* @param pendingIntent the body, pending intent for the notification
109+
* @param extras extras passed with the notification
110+
* @return the builder
111+
*/
112+
@TargetApi(Build.VERSION_CODES.O)
113+
private Notification buildNotification(String text, PendingIntent pendingIntent, Bundle extras,
114+
final CallInvite callInvite,
115+
int notificationId,
116+
String channelId) {
117+
Intent rejectIntent = new Intent(getApplicationContext(), IncomingCallNotificationService.class);
118+
rejectIntent.setAction(ACTION_REJECT);
119+
rejectIntent.putExtra(INCOMING_CALL_INVITE, callInvite);
120+
rejectIntent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
121+
PendingIntent piRejectIntent = PendingIntent.getService(getApplicationContext(), 0, rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT);
122+
123+
Intent acceptIntent = new Intent(getApplicationContext(), IncomingCallNotificationService.class);
124+
acceptIntent.setAction(ACTION_ACCEPT);
125+
acceptIntent.putExtra(INCOMING_CALL_INVITE, callInvite);
126+
acceptIntent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
127+
PendingIntent piAcceptIntent = PendingIntent.getService(getApplicationContext(), 0, acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT);
128+
129+
Notification.Builder builder =
130+
new Notification.Builder(getApplicationContext(), channelId)
131+
.setSmallIcon(R.drawable.ic_call_white_24dp)
132+
.setContentTitle("Incoming call")
133+
.setContentText(text)
134+
.setCategory(Notification.CATEGORY_CALL)
135+
.setFullScreenIntent(pendingIntent, true)
136+
.setExtras(extras)
137+
.setAutoCancel(true)
138+
.addAction(android.R.drawable.ic_menu_delete, getString(R.string.decline), piRejectIntent)
139+
.addAction(android.R.drawable.ic_menu_call, getString(R.string.answer), piAcceptIntent)
140+
.setFullScreenIntent(pendingIntent, true);
141+
142+
return builder.build();
143+
}
144+
145+
@TargetApi(Build.VERSION_CODES.O)
146+
private String createChannel(int channelImportance) {
147+
NotificationChannel callInviteChannel = new NotificationChannel(Constants.VOICE_CHANNEL_HIGH_IMPORTANCE,
148+
"Primary Voice Channel", NotificationManager.IMPORTANCE_HIGH);;
149+
String channelId = Constants.VOICE_CHANNEL_HIGH_IMPORTANCE;
150+
151+
if (channelImportance == NotificationManager.IMPORTANCE_LOW) {
152+
callInviteChannel = new NotificationChannel(Constants.VOICE_CHANNEL_LOW_IMPORTANCE,
153+
"Primary Voice Channel", NotificationManager.IMPORTANCE_LOW);;
154+
channelId = Constants.VOICE_CHANNEL_LOW_IMPORTANCE;
155+
}
156+
callInviteChannel.setLightColor(Color.GREEN);
157+
callInviteChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
158+
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
159+
notificationManager.createNotificationChannel(callInviteChannel);
160+
161+
return channelId;
162+
}
163+
164+
private void accept(CallInvite callInvite, int notificationId) {
165+
endForeground();
166+
Intent activeCallIntent = new Intent(this, getMainActivityClass(this));
167+
activeCallIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
168+
activeCallIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
169+
activeCallIntent.putExtra(INCOMING_CALL_INVITE, callInvite);
170+
activeCallIntent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
171+
activeCallIntent.setAction(ACTION_ACCEPT);
172+
startActivity(activeCallIntent);
173+
}
174+
175+
private void reject(CallInvite callInvite) {
176+
endForeground();
177+
callInvite.reject(getApplicationContext());
178+
}
179+
180+
private void handleCancelledCall(Intent intent) {
181+
endForeground();
182+
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
183+
}
184+
185+
private void handleIncomingCall(CallInvite callInvite, int notificationId) {
186+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
187+
setCallInProgressNotification(callInvite, notificationId);
188+
}
189+
sendCallInviteToActivity(callInvite, notificationId);
190+
}
191+
192+
private void endForeground() {
193+
stopForeground(true);
194+
}
195+
196+
private void setCallInProgressNotification(CallInvite callInvite, int notificationId) {
197+
if (isAppVisible()) {
198+
Log.i(TAG, "setCallInProgressNotification - app is visible.");
199+
startForeground(notificationId, createNotification(callInvite, notificationId, NotificationManager.IMPORTANCE_LOW));
200+
} else {
201+
Log.i(TAG, "setCallInProgressNotification - app is NOT visible.");
202+
startForeground(notificationId, createNotification(callInvite, notificationId, NotificationManager.IMPORTANCE_HIGH));
203+
}
204+
}
205+
206+
/*
207+
* Send the CallInvite to the VoiceActivity. Start the activity if it is not running already.
208+
*/
209+
private void sendCallInviteToActivity(CallInvite callInvite, int notificationId) {
210+
if (Build.VERSION.SDK_INT >= 29 && !isAppVisible()) {
211+
return;
212+
}
213+
Intent intent = new Intent(this, getMainActivityClass(this));
214+
intent.setAction(ACTION_INCOMING_CALL);
215+
intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
216+
intent.putExtra(INCOMING_CALL_INVITE, callInvite);
217+
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
218+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
219+
this.startActivity(intent);
220+
}
221+
222+
private boolean isAppVisible() {
223+
return ProcessLifecycleOwner
224+
.get()
225+
.getLifecycle()
226+
.getCurrentState()
227+
.isAtLeast(Lifecycle.State.STARTED);
228+
}
229+
}

0 commit comments

Comments
 (0)