Skip to content

Commit

Permalink
feat: Android Twilio SDK 5.0.1
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 Nov 15, 2020
1 parent 64c59fa commit 98c7b0f
Show file tree
Hide file tree
Showing 9 changed files with 333 additions and 78 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.1'
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,23 @@
import java.util.List;

import static android.content.Context.ACTIVITY_SERVICE;

import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_ANSWER_CALL;
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_REJECT_CALL;
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_HANGUP_CALL;
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_INCOMING_CALL;
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_MISSED_CALL;
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.Constants.NOTIFICATION_TYPE;
import static com.hoxfon.react.RNTwilioVoice.Constants.CALL_SID_KEY;
import static com.hoxfon.react.RNTwilioVoice.Constants.INCOMING_NOTIFICATION_PREFIX;
import static com.hoxfon.react.RNTwilioVoice.Constants.MISSED_CALLS_GROUP;
import static com.hoxfon.react.RNTwilioVoice.Constants.MISSED_CALLS_NOTIFICATION_ID;
import static com.hoxfon.react.RNTwilioVoice.Constants.HANGUP_NOTIFICATION_ID;
import static com.hoxfon.react.RNTwilioVoice.Constants.PREFERENCE_KEY;
import static com.hoxfon.react.RNTwilioVoice.Constants.ACTION_CLEAR_MISSED_CALLS_COUNT;
import static com.hoxfon.react.RNTwilioVoice.Constants.CLEAR_MISSED_CALLS_NOTIFICATION_ID;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.TAG;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_ANSWER_CALL;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_REJECT_CALL;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_HANGUP_CALL;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_INCOMING_CALL;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_MISSED_CALL;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_INVITE;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_CALL_NOTIFICATION_ID;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.NOTIFICATION_TYPE;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.CALL_SID_KEY;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.INCOMING_NOTIFICATION_PREFIX;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.MISSED_CALLS_GROUP;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.MISSED_CALLS_NOTIFICATION_ID;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.HANGUP_NOTIFICATION_ID;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.PREFERENCE_KEY;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.ACTION_CLEAR_MISSED_CALLS_COUNT;
import static com.hoxfon.react.RNTwilioVoice.TwilioVoiceModule.CLEAR_MISSED_CALLS_NOTIFICATION_ID;


public class CallNotificationManager {

Expand Down Expand Up @@ -72,7 +70,7 @@ public int getApplicationImportance(ReactApplicationContext context) {
return 0;
}

public Class getMainActivityClass(ReactApplicationContext context) {
public static Class getMainActivityClass(Context context) {
String packageName = context.getPackageName();
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
String className = launchIntent.getComponent().getClassName();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.hoxfon.react.RNTwilioVoice;

public class Constants {
public static final String INCOMING_NOTIFICATION_PREFIX = "Incoming_";
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.TwilioVoice.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 INCOMING_CALL_NOTIFICATION_ID = "INCOMING_CALL_NOTIFICATION_ID";
public static final String ACTION_ACCEPT = "ACTION_ACCEPT";
public static final String ACTION_REJECT = "ACTION_REJECT";
public static final String ACTION_MISSED_CALL = "MISSED_CALL";
public static final String ACTION_ANSWER_CALL = "ANSWER_CALL";
public static final String ACTION_REJECT_CALL = "REJECT_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_CANCEL_CALL_INVITE = "CANCEL_CALL_INVITE";
public static final String ACTION_CLEAR_MISSED_CALLS_COUNT = "CLEAR_MISSED_CALLS_COUNT";

public static final String NOTIFICATION_TYPE = "NOTIFICATION_TYPE";
public static final String CANCELLED_CALL_INVITE_ERR = "CANCELLED_CALL_INVITE_EXCEPTION";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
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;

public class IncomingCallNotificationService extends Service {

private static final String TAG = IncomingCallNotificationService.class.getSimpleName();

@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());

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return buildNotification(callInvite.getFrom() + " is calling.",
pendingIntent,
extras,
callInvite,
notificationId,
createChannel(channelImportance));
} else {
return new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_call_white_24dp)
.setContentTitle("Incoming call")
.setContentText(callInvite.getFrom() + " is calling.")
.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("Incoming call")
.setContentText(text)
.setCategory(Notification.CATEGORY_CALL)
.setFullScreenIntent(pendingIntent, true)
.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) {
NotificationChannel callInviteChannel = new NotificationChannel(Constants.VOICE_CHANNEL_HIGH_IMPORTANCE,
"Primary Voice Channel", NotificationManager.IMPORTANCE_HIGH);;
String channelId = Constants.VOICE_CHANNEL_HIGH_IMPORTANCE;

if (channelImportance == NotificationManager.IMPORTANCE_LOW) {
callInviteChannel = new NotificationChannel(Constants.VOICE_CHANNEL_LOW_IMPORTANCE,
"Primary Voice Channel", NotificationManager.IMPORTANCE_LOW);;
channelId = Constants.VOICE_CHANNEL_LOW_IMPORTANCE;
}
callInviteChannel.setLightColor(Color.GREEN);
callInviteChannel.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE);
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);
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) {
if (isAppVisible()) {
Log.i(TAG, "setCallInProgressNotification - app is visible.");
startForeground(notificationId, createNotification(callInvite, notificationId, NotificationManager.IMPORTANCE_LOW));
} else {
Log.i(TAG, "setCallInProgressNotification - app is NOT visible.");
startForeground(notificationId, createNotification(callInvite, notificationId, NotificationManager.IMPORTANCE_HIGH));
}
}

/*
* Send the CallInvite to the VoiceActivity. Start the activity if it is not running already.
*/
private void sendCallInviteToActivity(CallInvite callInvite, int notificationId) {
if (Build.VERSION.SDK_INT >= 29 && !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 98c7b0f

Please sign in to comment.