Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ buildscript {
'androidGradlePlugin': '7.4.2',
'googleServices' : '4.3.10',
'voiceAndroid' : '6.7.1',
'androidxCore' : '1.10.1',
'androidxCore' : '1.12.0',
'androidxLifecycle' : '2.2.0',
'audioSwitch' : '1.1.8',
'firebaseMessaging' : '23.4.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,15 @@ public static boolean isFirebaseServiceEnabled(Context context) {
return context.getResources()
.getBoolean(R.bool.twiliovoicereactnative_firebasemessagingservice_enabled);
}

/**
* Get configuration boolean, used to determine if full screen notifications are enabled
* or not.
* @param context the application context
* @return a boolean read from the application resources
*/
public static boolean isFullScreenNotificationEnabled(Context context) {
return context.getResources()
.getBoolean(R.bool.twiliovoicereactnative_fullscreennotification_enabled);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import com.twilio.voice.CallInvite;

import static com.twiliovoicereactnative.ConfigurationProperties.isFullScreenNotificationEnabled;
import static com.twiliovoicereactnative.Constants.VOICE_CHANNEL_DEFAULT_IMPORTANCE;
import static com.twiliovoicereactnative.Constants.VOICE_CHANNEL_HIGH_IMPORTANCE;
import static com.twiliovoicereactnative.Constants.VOICE_CHANNEL_LOW_IMPORTANCE;
Expand Down Expand Up @@ -165,16 +166,18 @@ public static Notification createIncomingCallNotification(@NonNull Context conte
callRecord.getUuid());
PendingIntent piAcceptIntent = constructPendingIntentForActivity(context, acceptIntent);

return constructNotificationBuilder(context, channelImportance)
NotificationCompat.Builder builder = constructNotificationBuilder(context, channelImportance)
.setSmallIcon(notificationResource.getSmallIconId())
.setCategory(Notification.CATEGORY_CALL)
.setAutoCancel(true)
.setContentIntent(piForegroundIntent)
.setFullScreenIntent(piForegroundIntent, true)
.addPerson(incomingCaller)
.setStyle(NotificationCompat.CallStyle.forIncomingCall(
incomingCaller, piRejectIntent, piAcceptIntent))
.build();
incomingCaller, piRejectIntent, piAcceptIntent));
if (isFullscreenIntentEnabled(context)) {
builder.setFullScreenIntent(piForegroundIntent, true);
}
return builder.build();
}

public static Notification createCallAnsweredNotificationWithLowImportance(@NonNull Context context,
Expand Down Expand Up @@ -202,16 +205,18 @@ public static Notification createCallAnsweredNotificationWithLowImportance(@NonN
callRecord.getUuid());
PendingIntent piEndCallIntent = constructPendingIntentForService(context, endCallIntent);

return constructNotificationBuilder(context, Constants.VOICE_CHANNEL_LOW_IMPORTANCE)
NotificationCompat.Builder builder = constructNotificationBuilder(context, Constants.VOICE_CHANNEL_LOW_IMPORTANCE)
.setSmallIcon(notificationResource.getSmallIconId())
.setCategory(Notification.CATEGORY_CALL)
.setAutoCancel(false)
.setContentIntent(piForegroundIntent)
.setFullScreenIntent(piForegroundIntent, true)
.setOngoing(true)
.addPerson(activeCaller)
.setStyle(NotificationCompat.CallStyle.forOngoingCall(activeCaller, piEndCallIntent))
.build();
.setStyle(NotificationCompat.CallStyle.forOngoingCall(activeCaller, piEndCallIntent));
if (isFullscreenIntentEnabled(context)) {
builder.setFullScreenIntent(piForegroundIntent, true);
}
return builder.build();
}

public static Notification createOutgoingCallNotificationWithLowImportance(@NonNull Context context,
Expand Down Expand Up @@ -239,16 +244,18 @@ public static Notification createOutgoingCallNotificationWithLowImportance(@NonN
callRecord.getUuid());
PendingIntent piEndCallIntent = constructPendingIntentForService(context, endCallIntent);

return constructNotificationBuilder(context, Constants.VOICE_CHANNEL_LOW_IMPORTANCE)
NotificationCompat.Builder builder = constructNotificationBuilder(context, Constants.VOICE_CHANNEL_LOW_IMPORTANCE)
.setSmallIcon(notificationResource.getSmallIconId())
.setCategory(Notification.CATEGORY_CALL)
.setAutoCancel(false)
.setContentIntent(piForegroundIntent)
.setFullScreenIntent(piForegroundIntent, true)
.setOngoing(true)
.addPerson(activeCaller)
.setStyle(NotificationCompat.CallStyle.forOngoingCall(activeCaller, piEndCallIntent))
.build();
.setStyle(NotificationCompat.CallStyle.forOngoingCall(activeCaller, piEndCallIntent));
if (isFullscreenIntentEnabled(context)) {
builder.setFullScreenIntent(piForegroundIntent, true);
}
return builder.build();
}

public static void createNotificationChannels(@NonNull Context context) {
Expand All @@ -272,6 +279,11 @@ public static void destroyNotificationChannels(@NonNull Context context) {
notificationManager.deleteNotificationChannelGroup(Constants.VOICE_CHANNEL_GROUP);
}

public static boolean isFullscreenIntentEnabled(Context context) {
return isFullScreenNotificationEnabled(context) &&
NotificationManagerCompat.from(context).canUseFullScreenIntent();
}

private static NotificationChannelCompat createNotificationChannel(@NonNull Context context,
@NonNull final String voiceChannelId) {
final int notificationImportance = getChannelImportance(voiceChannelId);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.twiliovoicereactnative;

import androidx.annotation.NonNull;
import androidx.core.app.NotificationManagerCompat;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
Expand Down Expand Up @@ -37,6 +38,7 @@
import static com.twiliovoicereactnative.CommonConstants.VoiceEventError;
import static com.twiliovoicereactnative.CommonConstants.VoiceEventRegistered;
import static com.twiliovoicereactnative.CommonConstants.VoiceEventUnregistered;
import static com.twiliovoicereactnative.ConfigurationProperties.isFullScreenNotificationEnabled;
import static com.twiliovoicereactnative.JSEventEmitter.constructJSMap;
import static com.twiliovoicereactnative.ReactNativeArgumentsSerializer.serializeCall;
import static com.twiliovoicereactnative.ReactNativeArgumentsSerializer.serializeCallInvite;
Expand All @@ -46,8 +48,12 @@
import static com.twiliovoicereactnative.ReactNativeArgumentsSerializer.*;

import android.annotation.SuppressLint;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.util.Pair;

import com.twiliovoicereactnative.CallRecordDatabase.CallRecord;
Expand Down Expand Up @@ -138,67 +144,6 @@ public void removeListeners(Integer count) {
logger.debug("Calling removeListeners: " + count);
}

@Override
@NonNull
public String getName() {
return TAG;
}

private RegistrationListener createRegistrationListener(Promise promise) {
return new RegistrationListener() {
@Override
public void onRegistered(@NonNull String accessToken, @NonNull String fcmToken) {
logger.log("Successfully registered FCM");
sendJSEvent(constructJSMap(new Pair<>(VoiceEventType, VoiceEventRegistered)));
promise.resolve(null);
}

@Override
public void onError(@NonNull RegistrationException registrationException,
@NonNull String accessToken,
@NonNull String fcmToken) {
String errorMessage = reactContext.getString(
R.string.registration_error,
registrationException.getErrorCode(),
registrationException.getMessage());
logger.error(errorMessage);

sendJSEvent(constructJSMap(
new Pair<>(VoiceEventType, VoiceEventError),
new Pair<>(VoiceErrorKeyError, serializeVoiceException(registrationException))));

promise.reject(errorMessage);
}
};
}

private UnregistrationListener createUnregistrationListener(Promise promise) {
return new UnregistrationListener() {
@Override
public void onUnregistered(String accessToken, String fcmToken) {
logger.log("Successfully unregistered FCM");
sendJSEvent(constructJSMap(new Pair<>(VoiceEventType, VoiceEventUnregistered)));
promise.resolve(null);
}

@Override
public void onError(RegistrationException registrationException, String accessToken, String fcmToken) {
@SuppressLint("DefaultLocale")
String errorMessage = reactContext.getString(
R.string.unregistration_error,
registrationException.getErrorCode(),
registrationException.getMessage());
logger.error(errorMessage);

sendJSEvent(constructJSMap(
new Pair<>(VoiceEventType, VoiceEventError),
new Pair<>(VoiceErrorKeyError, serializeVoiceException(registrationException))));

promise.reject(errorMessage);
}
};
}

@ReactMethod
public void voice_connect_android(
String accessToken,
Expand Down Expand Up @@ -690,6 +635,87 @@ public void callInvite_reject(String uuid, Promise promise) {
});
}

@ReactMethod
public void system_isFullScreenNotificationEnabled(Promise promise) {
boolean isEnabled =
isFullScreenNotificationEnabled(reactContext) &&
NotificationManagerCompat.from(reactContext).canUseFullScreenIntent();
promise.resolve(isEnabled);
}

@ReactMethod
public void system_requestFullScreenNotification(Promise promise) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU &&
isFullScreenNotificationEnabled(reactContext)) {
Intent intent = new Intent(
Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT,
Uri.parse("package:" + reactContext.getPackageName()));
reactContext.startActivity(intent);
}
promise.resolve(null);
}

@Override
@NonNull
public String getName() {
return TAG;
}

private RegistrationListener createRegistrationListener(Promise promise) {
return new RegistrationListener() {
@Override
public void onRegistered(@NonNull String accessToken, @NonNull String fcmToken) {
logger.log("Successfully registered FCM");
sendJSEvent(constructJSMap(new Pair<>(VoiceEventType, VoiceEventRegistered)));
promise.resolve(null);
}

@Override
public void onError(@NonNull RegistrationException registrationException,
@NonNull String accessToken,
@NonNull String fcmToken) {
String errorMessage = reactContext.getString(
R.string.registration_error,
registrationException.getErrorCode(),
registrationException.getMessage());
logger.error(errorMessage);

sendJSEvent(constructJSMap(
new Pair<>(VoiceEventType, VoiceEventError),
new Pair<>(VoiceErrorKeyError, serializeVoiceException(registrationException))));

promise.reject(errorMessage);
}
};
}

private UnregistrationListener createUnregistrationListener(Promise promise) {
return new UnregistrationListener() {
@Override
public void onUnregistered(String accessToken, String fcmToken) {
logger.log("Successfully unregistered FCM");
sendJSEvent(constructJSMap(new Pair<>(VoiceEventType, VoiceEventUnregistered)));
promise.resolve(null);
}

@Override
public void onError(RegistrationException registrationException, String accessToken, String fcmToken) {
@SuppressLint("DefaultLocale")
String errorMessage = reactContext.getString(
R.string.unregistration_error,
registrationException.getErrorCode(),
registrationException.getMessage());
logger.error(errorMessage);

sendJSEvent(constructJSMap(
new Pair<>(VoiceEventType, VoiceEventError),
new Pair<>(VoiceErrorKeyError, serializeVoiceException(registrationException))));

promise.reject(errorMessage);
}
};
}

/**
* Use the score map to get a Call.Score value from a string.
* @param score The score as a string passed from the JS layer.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package com.twiliovoicereactnative;

import static com.twiliovoicereactnative.ConfigurationProperties.isFullScreenNotificationEnabled;

import java.util.List;
import java.util.Vector;

import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.Window;
import android.view.WindowManager;

Expand Down
1 change: 1 addition & 0 deletions android/src/main/res/values/config.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="twiliovoicereactnative_firebasemessagingservice_enabled">true</bool>
<bool name="twiliovoicereactnative_fullscreennotification_enabled">true</bool>
</resources>
25 changes: 25 additions & 0 deletions docs/disable-fullscree-notifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
The functionality detailed in this document was added in `1.5.1` of the
`@twilio/voice-react-native-sdk`.

# Using Full Screen Notifications on Android
The `@twilio/voice-react-native-sdk` Starting with Android 14 (API 34),
full screen intents are only available for alarm and phone calling
applications and require user approval to enable. This is a departure
from previous versions of Android.

This document provides details on how to disable the use of full screen
notifications if desired.

## Disabling Full Screen Notifications on Android
To disable full screen notifications, you can add a `config.xml` file
in the `src/main/res/values/` folder within your `android/app/`
folder. Including the following content within this file will disable
the use of full screen notifications.
```
<bool name="twiliovoicereactnative_fullscreennotification_enabled">false</bool>
```
See [this file](/android/src/main/res/values/config.xml) for more details.

With full screen notifications disabled, the SDK will not ask for user
permissions for enabling it and it will not use full screen
notifications for incoming phone calls.