Skip to content

Commit 458b0d4

Browse files
feat: Android Twilio SDK 5.0.2
- notification for incoming call when the app is in the background
1 parent 29eb5e6 commit 458b0d4

9 files changed

+624
-521
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.2'
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

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

0 commit comments

Comments
 (0)