Skip to content

Commit

Permalink
feat: Android Twilio SDK 5.0.2
Browse files Browse the repository at this point in the history
- notification for incoming call when the app is in the background
  • Loading branch information
fabriziomoscon committed Dec 12, 2020
1 parent 29eb5e6 commit 458b0d4
Show file tree
Hide file tree
Showing 9 changed files with 624 additions and 521 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This is a React-Native wrapper for [Twilio Programmable Voice SDK](https://www.t

## Twilio Programmable Voice SDK

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

## Breaking changes in v4.0.0
Expand Down
3 changes: 2 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,10 @@ dependencies {
def supportLibVersion = rootProject.hasProperty('supportLibVersion') ? rootProject.supportLibVersion : DEFAULT_SUPPORT_LIB_VERSION

implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'com.twilio:voice-android:4.5.0'
implementation 'com.twilio:voice-android:5.0.2'
implementation "com.android.support:appcompat-v7:$supportLibVersion"
implementation 'com.facebook.react:react-native:+'
implementation 'com.google.firebase:firebase-messaging:17.6.+'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
testImplementation 'junit:junit:4.12'
}
2 changes: 1 addition & 1 deletion android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ org.gradle.jvmargs=-Xmx1536m
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useAndroidX=true
android.enableJetifier=true
android.enableJetifier=true

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.hoxfon.react.RNTwilioVoice;

public class Constants {
public static final String MISSED_CALLS_GROUP = "MISSED_CALLS";
public static final int MISSED_CALLS_NOTIFICATION_ID = 1;
public static final int HANGUP_NOTIFICATION_ID = 11;
public static final int CLEAR_MISSED_CALLS_NOTIFICATION_ID = 21;
public static final String PREFERENCE_KEY = "com.hoxfon.react.RNTwilioVoice.PREFERENCE_FILE_KEY";

public static final String CALL_SID_KEY = "CALL_SID";
public static final String VOICE_CHANNEL_LOW_IMPORTANCE = "notification-channel-low-importance";
public static final String VOICE_CHANNEL_HIGH_IMPORTANCE = "notification-channel-high-importance";
public static final String INCOMING_CALL_INVITE = "INCOMING_CALL_INVITE";
public static final String CANCELLED_CALL_INVITE = "CANCELLED_CALL_INVITE";
public static final String CANCELLED_CALL_INVITE_ERROR = "CANCELLED_CALL_INVITE_ERROR";
public static final String INCOMING_CALL_NOTIFICATION_ID = "INCOMING_CALL_NOTIFICATION_ID";
public static final String ACTION_ACCEPT = "com.hoxfon.react.RNTwilioVoice.ACTION_ACCEPT";
public static final String ACTION_REJECT = "com.hoxfon.react.RNTwilioVoice.ACTION_REJECT";
public static final String ACTION_MISSED_CALL = "MISSED_CALL";
public static final String ACTION_HANGUP_CALL = "HANGUP_CALL";
public static final String ACTION_INCOMING_CALL_NOTIFICATION = "ACTION_INCOMING_CALL_NOTIFICATION";
public static final String ACTION_INCOMING_CALL = "ACTION_INCOMING_CALL";
public static final String ACTION_CANCEL_CALL = "ACTION_CANCEL_CALL";
public static final String ACTION_FCM_TOKEN = "ACTION_FCM_TOKEN";
public static final String ACTION_CLEAR_MISSED_CALLS_COUNT = "CLEAR_MISSED_CALLS_COUNT";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package com.hoxfon.react.RNTwilioVoice;

import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;

import androidx.core.app.NotificationCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import com.twilio.voice.CallInvite;

import static com.hoxfon.react.RNTwilioVoice.CallNotificationManager.getMainActivityClass;
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_ACCEPT;
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_CANCEL_CALL;
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_INCOMING_CALL;
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_INCOMING_CALL_NOTIFICATION;
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_REJECT;
import static com.hoxfon.react.RNTwilioVoice.Constants.CALL_SID_KEY;
import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_CALL_INVITE;
import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_CALL_NOTIFICATION_ID;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG;

public class IncomingCallNotificationService extends Service {

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();

CallInvite callInvite = intent.getParcelableExtra(INCOMING_CALL_INVITE);
int notificationId = intent.getIntExtra(INCOMING_CALL_NOTIFICATION_ID, 0);

switch (action) {
case ACTION_INCOMING_CALL:
handleIncomingCall(callInvite, notificationId);
break;
case ACTION_ACCEPT:
accept(callInvite, notificationId);
break;
case ACTION_REJECT:
reject(callInvite);
break;
case ACTION_CANCEL_CALL:
handleCancelledCall(intent);
break;
default:
break;
}
return START_NOT_STICKY;
}

@Override
public IBinder onBind(Intent intent) {
return null;
}

private Notification createNotification(CallInvite callInvite, int notificationId, int channelImportance) {
Intent intent = new Intent(this, getMainActivityClass(this));
intent.setAction(ACTION_INCOMING_CALL_NOTIFICATION);
intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
intent.putExtra(INCOMING_CALL_INVITE, callInvite);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
PendingIntent pendingIntent =
PendingIntent.getActivity(this, notificationId, intent, PendingIntent.FLAG_UPDATE_CURRENT);
/*
* Pass the notification id and call sid to use as an identifier to cancel the
* notification later
*/
Bundle extras = new Bundle();
extras.putString(CALL_SID_KEY, callInvite.getCallSid());

String contextText = callInvite.getFrom() + " is calling.";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// TODO make text configurable from app resources
return buildNotification(contextText,
pendingIntent,
extras,
callInvite,
notificationId,
createChannel(channelImportance));
} else {
return new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_call_white_24dp)
.setContentTitle(getString(R.string.call_incoming))
.setContentText(contextText)
.setAutoCancel(true)
.setExtras(extras)
.setContentIntent(pendingIntent)
.setGroup("test_app_notification")
.setColor(Color.rgb(214, 10, 37))
.build();
}
}

/**
* Build a notification.
*
* @param text the text of the notification
* @param pendingIntent the body, pending intent for the notification
* @param extras extras passed with the notification
* @return the builder
*/
@TargetApi(Build.VERSION_CODES.O)
private Notification buildNotification(String text,
PendingIntent pendingIntent,
Bundle extras,
final CallInvite callInvite,
int notificationId,
String channelId) {
Intent rejectIntent = new Intent(getApplicationContext(), IncomingCallNotificationService.class);
rejectIntent.setAction(ACTION_REJECT);
rejectIntent.putExtra(INCOMING_CALL_INVITE, callInvite);
rejectIntent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
PendingIntent piRejectIntent = PendingIntent.getService(getApplicationContext(), 0, rejectIntent, PendingIntent.FLAG_UPDATE_CURRENT);

Intent acceptIntent = new Intent(getApplicationContext(), IncomingCallNotificationService.class);
acceptIntent.setAction(ACTION_ACCEPT);
acceptIntent.putExtra(INCOMING_CALL_INVITE, callInvite);
acceptIntent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
PendingIntent piAcceptIntent = PendingIntent.getService(getApplicationContext(), 0, acceptIntent, PendingIntent.FLAG_UPDATE_CURRENT);

Notification.Builder builder =
new Notification.Builder(getApplicationContext(), channelId)
.setSmallIcon(R.drawable.ic_call_white_24dp)
.setContentTitle(getString(R.string.call_incoming))
.setContentText(text)
.setCategory(Notification.CATEGORY_CALL)
.setExtras(extras)
.setAutoCancel(true)
.addAction(android.R.drawable.ic_menu_delete, getString(R.string.decline), piRejectIntent)
.addAction(android.R.drawable.ic_menu_call, getString(R.string.answer), piAcceptIntent)
.setFullScreenIntent(pendingIntent, true);

return builder.build();
}

@TargetApi(Build.VERSION_CODES.O)
private String createChannel(int channelImportance) {
String channelId = Constants.VOICE_CHANNEL_HIGH_IMPORTANCE;
if (channelImportance == NotificationManager.IMPORTANCE_LOW) {
channelId = Constants.VOICE_CHANNEL_LOW_IMPORTANCE;
}
NotificationChannel callInviteChannel = new NotificationChannel(channelId,
"Incoming calls", channelImportance);
callInviteChannel.setLightColor(Color.GREEN);
// TODO set sound for background incoming call
// callInviteChannel.setSound();
callInviteChannel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);
NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.createNotificationChannel(callInviteChannel);

return channelId;
}

private void accept(CallInvite callInvite, int notificationId) {
endForeground();
Intent activeCallIntent = new Intent(this, getMainActivityClass(this));
activeCallIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
activeCallIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activeCallIntent.putExtra(INCOMING_CALL_INVITE, callInvite);
activeCallIntent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
activeCallIntent.setAction(ACTION_ACCEPT);
this.startActivity(activeCallIntent);
}

private void reject(CallInvite callInvite) {
endForeground();
callInvite.reject(getApplicationContext());
}

private void handleCancelledCall(Intent intent) {
endForeground();
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}

private void handleIncomingCall(CallInvite callInvite, int notificationId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
setCallInProgressNotification(callInvite, notificationId);
}
sendCallInviteToActivity(callInvite, notificationId);
}

private void endForeground() {
stopForeground(true);
}

private void setCallInProgressNotification(CallInvite callInvite, int notificationId) {
int importance = NotificationManager.IMPORTANCE_LOW;
if (!isAppVisible()) {
Log.i(TAG, "setCallInProgressNotification - app is NOT visible.");
importance = NotificationManager.IMPORTANCE_HIGH;
}
this.startForeground(notificationId, createNotification(callInvite, notificationId, importance));
}

/*
* Send the CallInvite to the Activity. Start the activity if it is not running already.
*/
private void sendCallInviteToActivity(CallInvite callInvite, int notificationId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !isAppVisible()) {
return;
}
Intent intent = new Intent(this, getMainActivityClass(this));
intent.setAction(ACTION_INCOMING_CALL);
intent.putExtra(INCOMING_CALL_NOTIFICATION_ID, notificationId);
intent.putExtra(INCOMING_CALL_INVITE, callInvite);
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
this.startActivity(intent);
}

private boolean isAppVisible() {
return ProcessLifecycleOwner
.get()
.getLifecycle()
.getCurrentState()
.isAtLeast(Lifecycle.State.STARTED);
}
}
Loading

0 comments on commit 458b0d4

Please sign in to comment.