How to add VoIP Push Notification to your React Native VoIP App (Part 2)
In the previous part, I discussed the problems arising with trying to add an Incoming Call page to your React Native VoIP App with Pure JavaScript. Today I will discuss a potential solution and the trade-offs it brings with it.
All the problems I encountered centered on the fact the code I wanted to execute was in JavaScript and I needed the OS to execute that code through the React Native Bridge. While impressive, it is quite difficult to execute code through the bridge once the app has left the foreground. The solution I propose involves writing some native code(Swift and Java). In this Part, I will expand on the Android implementation with Java from the Receipt of the VoIP Notification to the display of the IncomingCall screen.
Android Setup
First, set up your android app to receive notifications from FCM. Then create a class called NotificationService that would handle all the actual notification creation and display. In the NotificationService class add Context as a global variable and as an input parameter. The NotificationService class should look like this.
public class NotificationService {
private final Context context;
public NotificationService(Context context) {
this.context = context;
}
}
Add a method called displayVoipNotification. It takes a Map containing the notification payload from Firebase and returns void.
public void displayVoIPNotification(Map<String, String> payload){
}
In the FirebaseMessagingService class that you created during the Setup, override the onMessageReceived method and add a call to the displayVoipNotification of the NotificationService class.
@Override
public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
new NotificationService(this).displayVoIPNotification(remoteMessage.getData());
}
Now we can focus on creating and showing the VoIP Notification for Android.
Creating and Displaying Android VoIP Notification
Creating a VoIP Notification Channel
Starting in Android 8.0 (API level 26), all notifications must be assigned to a channel. For each channel, you can set the visual and auditory behavior that is applied to all notifications in that channel.
Creating a channel that displays Alert Notifications and creating a channel that displays VoIP notifications are slightly different. In your NotificationService, add a new method called createVoipNotificationChannel that you will call in the onCreate method of your activity. This method will run every time you start your app and will only run the first time, so no need to worry about creating multiple channels with the same name.
public void createVoIPNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = context.getString(R.string.channel_name);
String description = context.getString(R.string.channel_description);
int importance = NotificationManager.IMPORTANCE_HIGH;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
channel.setDescription(description);
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
You would need to add a global variable called CHANNEL_ID and assign it a random value (maybe "VOIP_CHANNEL") and two strings in your strings.xml file called channel_name and channel_description. Your strings.xml file will look similar to this:
<resources>
<string name="app_name">VoIP App</string>
<string name="channel_name">notification_channel</string>
<string name="channel_description">Notification Channel for VoIP Calls</string>
</resources>
With that, you have a working Notification channel for displaying your VoIP notifications.
Creating an Incoming Call Activity {#76b1 .graf .graf--h4 .graf-after--p name="76b1"}
This is needed if you would like to give the OS the option to show a full screen activity instead of an heads up notification. The methods of the activity will be :
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
if (intent == null || intent.getExtras() == null) {
Toast.makeText(this, "Incomplete details to join call", Toast.LENGTH_SHORT).show();
finish();
return;
} else if (intent.getExtras().containsKey("notificationId")) {
new NotificationService(this)
.cancelNotification(intent.getExtras().getInt("notificationId"));
}
setContentView(R.layout.activity_incoming_call);
playRingtone();
FrameLayout answerCallButton = findViewById(R.id.answerCallButton);
FrameLayout rejectCallButton = findViewById(R.id.rejectCallButton);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
| WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
| WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
answerCallButton.setOnClickListener(view -> {
Toast.makeText(this, "Answer Call", Toast.LENGTH_SHORT).show();
stopRingtone();
finish();
});
rejectCallButton.setOnClickListener(view -> {
Toast.makeText(this, "Reject Call", Toast.LENGTH_SHORT).show();
finish();
});
}
private void playRingtone() {
mediaPlayer = MediaPlayer.create(this, R.raw.ringtone);
mediaPlayer.setLooping(true);
mediaPlayer.start();
vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
long[] pattern = {0, 1000, 1000};
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
vibrator.vibrate(VibrationEffect.createWaveform(pattern, 0));
} else {
vibrator.vibrate(pattern, 0);
}
}
private void stopRingtone() {
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer = null;
}
if (vibrator != null) {
vibrator.cancel();
vibrator = null;
}
}
@Override
protected void onDestroy() {
super.onDestroy();
stopRingtone();
}
There are 2 global variables.
private MediaPlayer mediaPlayer;
private Vibrator vibrator;
And the activity_incoming_call.xml should contain your two buttons by the id answerCallButton and rejectCallButton.
The onCreate is the busiest of all the methods. It checks if the intent passed has the details needed to join/accept a call. In your case, it could be the call id or the caller name. Then it checks if the activity was opened from a notification. If it is, it cancels the notification. Then it plays the ringtone and sets the click listeners.
The displayVoIPNotification method
public void displayVoIPNotification(Map<String, String> payload) {
int notificationId = new Random().nextInt(500);
String callId = payload.get("callId");
Bundle bundle = new Bundle();
bundle.putString("callId", callId);
bundle.putInt("notificationId", notificationId);
Intent incomingCallIntent = new Intent(context, IncomingCallActivity.class);
incomingCallIntent.putExtras(bundle);
incomingCallIntent.setFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION |
Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent incomingCallPendingIntent = PendingIntent.getActivity(context, 1,
incomingCallIntent, PendingIntent.FLAG_CANCEL_CURRENT);
Notification.Builder builder;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder = new Notification.Builder(context, CHANNEL_ID);
builder.setFullScreenIntent(incomingCallPendingIntent, true);
builder.setCategory(Notification.CATEGORY_CALL);
} else {
builder = new Notification.Builder(context);
}
builder.setContentText("Incoming Call");
builder.setContentIntent(incomingCallPendingIntent);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setContentTitle("VoIP App");
builder.setAutoCancel(true);
Notification notification = builder.build();
notification.flags = Notification.FLAG_INSISTENT;
builder.setFullScreenIntent(incomingCallPendingIntent, true);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.notify(notificationId, notification);
}
callId is an example of a payload item you would use to identify which call to join. notificationId is used to track the call notification so as to enable cancellation from the IncomingCallActivity
The cancelNotification method
The is the last method in our NotificationService class. It is pretty self explanatory.
public void cancelNotification(int id) {
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
notificationManager.cancel(id);
}
This is all the code you need to display the incoming call activity both when the app is in the background and the foreground.
In the following part, I will be discussing the proper way to send the voip notification payload to the device.
If you have any questions, you can leave a comment or reach me on Twitter.
