Skip to content

Commit

Permalink
added missed call notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
diego committed Apr 15, 2021
1 parent 86609f4 commit e7cd776
Show file tree
Hide file tree
Showing 23 changed files with 386 additions and 75 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
.packages
.pub/

build/
build/
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.0.3
* Added missed call notifications
* Updated Android SDK
* Added localization files

## 0.0.1

* Initial release
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ TwilioVoice.instance.setDefaultCallerName(String callerName)
```

### Call Events
use stream `TwilioVoice.instance.callEventsListener` to receive events from the TwilioSDK such as call events and logs, it is a broadcast so you can listen to it on different parts of your app.
use stream `TwilioVoice.instance.callEventsListener` to receive events from the TwilioSDK such as call events and logs, it is a broadcast so you can listen to it on different parts of your app. Some events might be missed when the app has not launched, please check out the example project to find the workarounds.

The events sent are the following
- ringing
Expand All @@ -86,6 +86,10 @@ The events sent are the following
- log
- answer

## showMissedCallNotifications
By default a local notification will be shown to the user after missing a call, clicking on the notification will call back the user. To remove this feature, set `showMissedCallNotifications` to `false`.



### Calls

Expand Down Expand Up @@ -138,4 +142,8 @@ You can use `TwilioVoice.instance.hasMicAccess` and `TwilioVoice.instance.reques

#### Background calls (Android only on some devices)
Xiami devices, and maybe others, need a spetial permission to receive background calls. use `TwilioVoice.instance.requiresBackgroundPermissions` to check if your device requires a special permission, if it does, show a rationale explaining the user why you need the permisison. Finally call
`TwilioVoice.instance.requestBackgroundPermissions` which will take the user to the App Settings page to enable the permission.
`TwilioVoice.instance.requestBackgroundPermissions` which will take the user to the App Settings page to enable the permission.


### Localization
Because some of the UI is in native code, you need to localize those strings natively in your project. You can find in the example project localization for spanish, PRs are welcome for other languages.
Binary file modified android/.idea/caches/build_file_checksums.ser
Binary file not shown.
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ android {

dependencies {
implementation platform('com.google.firebase:firebase-bom:26.0.0')
implementation 'com.twilio:voice-android:5.6.1'
implementation 'com.twilio:voice-android:5.7.2'
implementation 'com.google.firebase:firebase-messaging'
// implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
Expand Down
2 changes: 2 additions & 0 deletions android/src/main/java/com/twilio/twilio_voice/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class Constants {
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 CALL_FROM = "CALL_FROM";
public static final String CALL_TO = "CALL_TO";
public static final String CANCELLED_CALL_INVITE = "CANCELLED_CALL_INVITE";
public static final String ACCEPT_CALL_ORIGIN = "ACCEPT_CALL_ORIGIN";
public static final String INCOMING_CALL_NOTIFICATION_ID = "INCOMING_CALL_NOTIFICATION_ID";
Expand All @@ -18,5 +19,6 @@ public class Constants {
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_RETURN_CALL = "ACTION_RETURN_CALL";
public static final String ACTION_FCM_TOKEN = "ACTION_FCM_TOKEN";
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@
import android.util.Log;

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

import com.twilio.voice.CallInvite;
import com.twilio.voice.CancelledCallInvite;

public class IncomingCallNotificationService extends Service {

Expand All @@ -31,7 +33,7 @@ public class IncomingCallNotificationService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String action = intent.getAction();
Log.i(TAG, "onStartCommand "+ action);
Log.i(TAG, "onStartCommand " + action);
if (action != null) {
CallInvite callInvite = intent.getParcelableExtra(Constants.INCOMING_CALL_INVITE);
int notificationId = intent.getIntExtra(Constants.INCOMING_CALL_NOTIFICATION_ID, 0);
Expand All @@ -40,7 +42,7 @@ public int onStartCommand(Intent intent, int flags, int startId) {
handleIncomingCall(callInvite, notificationId);
break;
case Constants.ACTION_ACCEPT:
int origin = intent.getIntExtra(Constants.ACCEPT_CALL_ORIGIN,0);
int origin = intent.getIntExtra(Constants.ACCEPT_CALL_ORIGIN, 0);
Log.d(TAG, "onStartCommand-ActionAccept $origin");
accept(callInvite, notificationId, origin);
break;
Expand All @@ -50,6 +52,9 @@ public int onStartCommand(Intent intent, int flags, int startId) {
case Constants.ACTION_CANCEL_CALL:
handleCancelledCall(intent);
break;
case Constants.ACTION_RETURN_CALL:
returnCall(intent);
break;
default:
break;
}
Expand Down Expand Up @@ -79,12 +84,12 @@ private Notification createNotification(CallInvite callInvite, int notificationI

Context context = getApplicationContext();
SharedPreferences preferences = context.getSharedPreferences(TwilioPreferences, Context.MODE_PRIVATE);
Log.i(TAG, "Setting notification from, "+ callInvite.getFrom());
String fromId = callInvite.getFrom().replace("client:","");
Log.i(TAG, "Setting notification from, " + callInvite.getFrom());
String fromId = callInvite.getFrom().replace("client:", "");
String caller = preferences.getString(fromId, preferences.getString("defaultCaller", "Unknown caller"));

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return buildNotification(getApplicationName(context),getString(R.string.new_call,caller),
return buildNotification(getApplicationName(context), getString(R.string.new_call, caller),
pendingIntent,
extras,
callInvite,
Expand All @@ -95,19 +100,20 @@ private Notification createNotification(CallInvite callInvite, int notificationI
return new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_call_end_white_24dp)
.setContentTitle(getApplicationName(context))
.setContentText(getString(R.string.new_call,caller))
.setContentText(getString(R.string.new_call, caller))
.setAutoCancel(true)
.setOngoing(true)
.setExtras(extras)
.setContentIntent(pendingIntent)
.setFullScreenIntent(pendingIntent,true)
.setVibrate(new long[] { 1000, 1000, 1000, 1000, 1000, 1000, 1000 })
.setFullScreenIntent(pendingIntent, true)
.setVibrate(new long[]{1000, 1000, 1000, 1000, 1000, 1000, 1000})
.setLights(Color.RED, 3000, 3000)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setColor(Color.rgb(20, 10, 200)).build();
}
}

public static String getApplicationName(Context context) {
ApplicationInfo applicationInfo = context.getApplicationInfo();
int stringId = applicationInfo.labelRes;
Expand Down Expand Up @@ -206,8 +212,73 @@ private void reject(CallInvite callInvite) {
}

private void handleCancelledCall(Intent intent) {
CancelledCallInvite cancelledCallInvite = intent.getParcelableExtra(Constants.CANCELLED_CALL_INVITE);
SharedPreferences preferences = getApplicationContext().getSharedPreferences(TwilioPreferences, Context.MODE_PRIVATE);
boolean prefsShow = preferences.getBoolean("show-notifications", true);
if (prefsShow) {
buildMissedCallNotification(cancelledCallInvite.getFrom(), cancelledCallInvite.getTo());
}
endForeground();
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}

private void returnCall(Intent intent) {
endForeground();
Log.i(TAG, "returning call!!!!");
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.cancel(100);
}


private void buildMissedCallNotification(String callerId, String to) {

String fromId = callerId.replace("client:", "");
Context context = getApplicationContext();
SharedPreferences preferences = context.getSharedPreferences(TwilioPreferences, Context.MODE_PRIVATE);
String callerName = preferences.getString(fromId, preferences.getString("defaultCaller", "Unknown caller"));
String title = getString(R.string.notification_missed_call, callerName);


Intent returnCallIntent = new Intent(getApplicationContext(), IncomingCallNotificationService.class);
returnCallIntent.setAction(Constants.ACTION_RETURN_CALL);
returnCallIntent.putExtra(Constants.CALL_TO, to);
returnCallIntent.putExtra(Constants.CALL_FROM, callerId);
PendingIntent piReturnCallIntent = PendingIntent.getService(getApplicationContext(), 0, returnCallIntent, PendingIntent.FLAG_UPDATE_CURRENT);


Notification notification;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

NotificationCompat.Builder builder =
new NotificationCompat.Builder(this, createChannel(NotificationManager.IMPORTANCE_HIGH))


.setSmallIcon(R.drawable.ic_call_end_white_24dp)
.setContentTitle(title)
.setCategory(Notification.CATEGORY_CALL)
.setAutoCancel(true)
.addAction(android.R.drawable.ic_menu_call, getString(R.string.twilio_call_back), piReturnCallIntent)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentTitle(getApplicationName(context))
.setContentText(title)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);

notification = builder.build();
} else {
notification = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_call_end_white_24dp)
.setContentTitle(getApplicationName(context))
.setContentText(title)
.setAutoCancel(true)
.setOngoing(true)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setPriority(NotificationCompat.PRIORITY_MAX)
.addAction(android.R.drawable.ic_menu_call, getString(R.string.decline), piReturnCallIntent)
.setColor(Color.rgb(20, 10, 200)).build();
}
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(100, notification);
}

private void handleIncomingCall(CallInvite callInvite, int notificationId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry;

import static java.lang.Boolean.getBoolean;

public class TwilioVoicePlugin implements FlutterPlugin, MethodChannel.MethodCallHandler, EventChannel.StreamHandler,
ActivityAware, PluginRegistry.NewIntentListener {

Expand Down Expand Up @@ -164,6 +166,23 @@ private void handleIncomingCallIntent(Intent intent) {
backgroundCallUI = false;
disconnect();
break;
case Constants.ACTION_RETURN_CALL:

if(this.checkPermissionForMicrophone()){
final HashMap<String, String> params = new HashMap<>();

String to = intent.getStringExtra(Constants.CALL_FROM);
String from = intent.getStringExtra(Constants.CALL_TO);
Log.d(TAG, "calling: " + to);
params.put("To", to.replace("client:", ""));
sendPhoneCallEvents("ReturningCall|" + from + "|" + to + "|" + "Incoming");
this.callOutgoing = true;
final ConnectOptions connectOptions = new ConnectOptions.Builder(this.accessToken)
.params(params)
.build();
this.activeCall = Voice.connect(this.activity, connectOptions, this.callListener);
}
break;
default:
break;
}
Expand Down Expand Up @@ -193,6 +212,7 @@ private void handleReject() {

private void handleCancel() {
callOutgoing = false;
sendPhoneCallEvents("Missed Call");
sendPhoneCallEvents("Call Ended");
SoundPoolManager.getInstance(context).stopRinging();
Intent intent = new Intent(activity, AnswerJavaActivity.class);
Expand All @@ -213,6 +233,7 @@ private void registerReceiver() {
intentFilter.addAction(Constants.ACTION_REJECT);
intentFilter.addAction(Constants.ACTION_END_CALL);
intentFilter.addAction(Constants.ACTION_TOGGLE_MUTE);
intentFilter.addAction(Constants.ACTION_RETURN_CALL);
LocalBroadcastManager.getInstance(this.activity).registerReceiver(
voiceBroadcastReceiver, intentFilter);
isReceiverRegistered = true;
Expand Down Expand Up @@ -277,6 +298,7 @@ public void onReceive(Context context, Intent intent) {
case Constants.ACTION_ACCEPT:
case Constants.ACTION_TOGGLE_MUTE:
case Constants.ACTION_END_CALL:
case Constants.ACTION_RETURN_CALL:

/*
* Handle the incoming or cancelled call invite
Expand Down Expand Up @@ -448,7 +470,14 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
backgroundCallUI = true;
}


} else if (call.method.equals("show-notifications")) {
boolean show = call.argument("show");
boolean prefsShow = pSharedPref.getBoolean("show-notifications", true);
if(show != prefsShow){
SharedPreferences.Editor edit = pSharedPref.edit();
edit.putBoolean("show-notifications", show);
edit.apply();
}
} else if (call.method.equals("requiresBackgroundPermissions")) {
String manufacturer = "xiaomi";
if (manufacturer.equalsIgnoreCase(android.os.Build.MANUFACTURER)) {
Expand Down
3 changes: 3 additions & 0 deletions android/src/main/res/values-es-rMX/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@
<string name="new_call">Llamada de %s</string>
<string name="incoming_call_title">Llamada entrante...</string>
<string name="unknown_caller">Numero desconocido</string>
<string name="notification_missed_call">Llamada perdida de %s</string>
<string name="twilio_call_back">Llamar</string>

</resources>
2 changes: 2 additions & 0 deletions android/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@
<string name="new_call">Call from %s</string>
<string name="incoming_call_title">Incomming call...</string>
<string name="unknown_caller">Unknown caller</string>
<string name="notification_missed_call">Missed call from %s</string>
<string name="twilio_call_back">Call Back</string>
</resources>
1 change: 1 addition & 0 deletions example/ios/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
Runner/GoogleService-Info.plist
**/GoogleService-Info.plist

# Exceptions to above rules.
!default.mode1v3
Expand Down
22 changes: 14 additions & 8 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PODS:
- cloud_functions (1.0.0):
- cloud_functions (1.0.3):
- Firebase/Functions (= 7.3.0)
- firebase_core
- Flutter
Expand All @@ -14,14 +14,14 @@ PODS:
- Firebase/Messaging (7.3.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 7.3.0)
- firebase_auth (1.0.0):
- firebase_auth (1.1.0):
- Firebase/Auth (= 7.3.0)
- firebase_core
- Flutter
- firebase_core (1.0.0):
- firebase_core (1.0.3):
- Firebase/CoreOnly (= 7.3.0)
- Flutter
- firebase_messaging (9.0.0):
- firebase_messaging (9.1.1):
- Firebase/Messaging (= 7.3.0)
- firebase_core
- Flutter
Expand Down Expand Up @@ -60,6 +60,8 @@ PODS:
- GoogleUtilities/Reachability (~> 7.0)
- GoogleUtilities/UserDefaults (~> 7.0)
- Flutter (1.0.0)
- flutter_local_notifications (0.0.1):
- Flutter
- GoogleDataTransport (8.2.0):
- nanopb (~> 2.30907.0)
- GoogleUtilities/AppDelegateSwizzler (7.2.2):
Expand Down Expand Up @@ -97,6 +99,7 @@ DEPENDENCIES:
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- twilio_voice (from `.symlinks/plugins/twilio_voice/ios`)

SPEC REPOS:
Expand Down Expand Up @@ -127,15 +130,17 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter:
:path: Flutter
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
twilio_voice:
:path: ".symlinks/plugins/twilio_voice/ios"

SPEC CHECKSUMS:
cloud_functions: 32ea5ece6048a6029d3280df62afe8ff85598ce9
cloud_functions: c4ab28246ffa9b4391e21a7f93ae75171b6c6863
Firebase: 26223c695fe322633274198cb19dca8cb7e54416
firebase_auth: f07f3ee03813d95157ee08ce763c649c656174b5
firebase_core: f47224dd6a9b928b4cf0128b0d004ec9e47d4bf2
firebase_messaging: fc1811236795c2313b8339c35d31295b1cd8486f
firebase_auth: d09964a120e218411768f5ca2d3ce938abd319f2
firebase_core: b5d81dfd4fb2d6f700e67de34d9a633ae325c4e9
firebase_messaging: 7547c99f31466371f9cfcb733d5a1bf32a819872
FirebaseAuth: c224a0cf1afa0949bd5c7bfcf154b4f5ce8ddef2
FirebaseCore: 4d3c72622ce0e2106aaa07bb4b2935ba2c370972
FirebaseCoreDiagnostics: 179bf3831213451c8addd036aca7fcf5492ec154
Expand All @@ -144,6 +149,7 @@ SPEC CHECKSUMS:
FirebaseInstanceID: cf940324a20ac14a27ad1e931d15ac9d335526db
FirebaseMessaging: 68d1bcb14880189558a8ae57167abe0b7e417232
Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
GoogleDataTransport: 1024b1a4dfbd7a0e92cb20d7e0a6f1fb66b449a4
GoogleUtilities: 31c5b01f978a70c6cff2afc6272b3f1921614b43
GTMSessionFetcher: b3503b20a988c4e20cc189aa798fd18220133f52
Expand Down
Loading

0 comments on commit e7cd776

Please sign in to comment.