From b4281c5a7032662d8d424e326797d0a14c9bd59b Mon Sep 17 00:00:00 2001 From: Jon Chambers <63609320+jon-signal@users.noreply.github.com> Date: Fri, 12 Aug 2022 11:06:31 -0400 Subject: [PATCH] Send non-urgent push notifications with lower priority --- .../textsecuregcm/WhisperServerService.java | 2 +- .../dynamic/DynamicConfiguration.java | 8 ++ .../DynamicPushNotificationConfiguration.java | 18 ++++ .../textsecuregcm/push/APNSender.java | 30 +++++- .../push/ApnPushNotificationScheduler.java | 5 +- .../textsecuregcm/push/FcmSender.java | 2 +- .../textsecuregcm/push/MessageSender.java | 4 +- .../textsecuregcm/push/PushNotification.java | 3 +- .../push/PushNotificationManager.java | 100 ++++++++++------- .../textsecuregcm/push/ReceiptSender.java | 3 +- .../storage/ChangeNumberManager.java | 2 + .../AuthenticatedConnectListener.java | 2 +- .../textsecuregcm/push/APNSenderTest.java | 97 ++++++++++------- .../ApnPushNotificationSchedulerTest.java | 4 +- .../textsecuregcm/push/FcmSenderTest.java | 8 +- .../textsecuregcm/push/MessageSenderTest.java | 6 +- .../push/PushNotificationManagerTest.java | 101 +++++++++++++++--- 17 files changed, 280 insertions(+), 115 deletions(-) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicPushNotificationConfiguration.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index f3b9e09ca..3457b6582 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -455,7 +455,7 @@ public class WhisperServerService extends Application getExperimentEnrollmentConfiguration( final String experimentName) { return Optional.ofNullable(experiments.get(experimentName)); @@ -126,4 +130,8 @@ public class DynamicConfiguration { public DynamicMessagePersisterConfiguration getMessagePersisterConfiguration() { return messagePersister; } + + public DynamicPushNotificationConfiguration getPushNotificationConfiguration() { + return pushNotifications; + } } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicPushNotificationConfiguration.java b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicPushNotificationConfiguration.java new file mode 100644 index 000000000..f442d25c0 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/configuration/dynamic/DynamicPushNotificationConfiguration.java @@ -0,0 +1,18 @@ +/* + * Copyright 2013-2022 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.configuration.dynamic; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class DynamicPushNotificationConfiguration { + + @JsonProperty + private boolean lowUrgencyEnabled = false; + + public boolean isLowUrgencyEnabled() { + return lowUrgencyEnabled; + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java index 70c994c52..8eae39f2a 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/APNSender.java @@ -45,6 +45,11 @@ public class APNSender implements Managed, PushNotificationSender { .setLocalizedAlertMessage("APN_Message") .build(); + @VisibleForTesting + static final String APN_BACKGROUND_PAYLOAD = new SimpleApnsPayloadBuilder() + .setContentAvailable(true) + .build(); + @VisibleForTesting static final Instant MAX_EXPIRATION = Instant.ofEpochMilli(Integer.MAX_VALUE * 1000L); @@ -83,7 +88,13 @@ public class APNSender implements Managed, PushNotificationSender { final boolean isVoip = notification.tokenType() == PushNotification.TokenType.APN_VOIP; final String payload = switch (notification.notificationType()) { - case NOTIFICATION -> isVoip ? APN_VOIP_NOTIFICATION_PAYLOAD : APN_NSE_NOTIFICATION_PAYLOAD; + case NOTIFICATION -> { + if (isVoip) { + yield APN_VOIP_NOTIFICATION_PAYLOAD; + } else { + yield notification.urgent() ? APN_NSE_NOTIFICATION_PAYLOAD : APN_BACKGROUND_PAYLOAD; + } + } case CHALLENGE -> new SimpleApnsPayloadBuilder() .setSound("default") @@ -98,8 +109,19 @@ public class APNSender implements Managed, PushNotificationSender { .build(); }; + final PushType pushType; + + if (isVoip) { + pushType = PushType.VOIP; + } else { + pushType = notification.urgent() ? PushType.ALERT : PushType.BACKGROUND; + } + + final DeliveryPriority deliveryPriority = + (notification.urgent() || isVoip) ? DeliveryPriority.IMMEDIATE : DeliveryPriority.CONSERVE_POWER; + final String collapseId = - (notification.notificationType() == PushNotification.NotificationType.NOTIFICATION && !isVoip) + (notification.notificationType() == PushNotification.NotificationType.NOTIFICATION && notification.urgent() && !isVoip) ? "incoming-message" : null; final Instant start = Instant.now(); @@ -108,8 +130,8 @@ public class APNSender implements Managed, PushNotificationSender { topic, payload, MAX_EXPIRATION, - DeliveryPriority.IMMEDIATE, - isVoip ? PushType.VOIP : PushType.ALERT, + deliveryPriority, + pushType, collapseId)) .whenComplete((response, throwable) -> { // Note that we deliberately run this small bit of non-blocking measurement on the "send notification" thread diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationScheduler.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationScheduler.java index 104f2ba2c..10654059b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationScheduler.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationScheduler.java @@ -250,7 +250,7 @@ public class ApnPushNotificationScheduler implements Managed { return; } - apnSender.sendNotification(new PushNotification(apnId, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device)); + apnSender.sendNotification(new PushNotification(apnId, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device, true)); retry.increment(); } @@ -263,8 +263,7 @@ public class ApnPushNotificationScheduler implements Managed { getLastBackgroundNotificationTimestampKey(account, device), String.valueOf(clock.millis()), new SetArgs().ex(BACKGROUND_NOTIFICATION_PERIOD))); - // TODO Set priority, etc. - apnSender.sendNotification(new PushNotification(device.getApnId(), PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device)); + apnSender.sendNotification(new PushNotification(device.getApnId(), PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device, false)); backgroundNotificationSentCounter.increment(); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/FcmSender.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/FcmSender.java index 536293565..f13b72b28 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/FcmSender.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/FcmSender.java @@ -84,7 +84,7 @@ public class FcmSender implements PushNotificationSender { Message.Builder builder = Message.builder() .setToken(pushNotification.deviceToken()) .setAndroidConfig(AndroidConfig.builder() - .setPriority(AndroidConfig.Priority.HIGH) + .setPriority(pushNotification.urgent() ? AndroidConfig.Priority.HIGH : AndroidConfig.Priority.NORMAL) .build()); final String key = switch (pushNotification.notificationType()) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/MessageSender.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/MessageSender.java index 0d83babd1..e7b512e9e 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/MessageSender.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/MessageSender.java @@ -50,7 +50,7 @@ public class MessageSender { this.pushLatencyManager = pushLatencyManager; } - public void sendMessage(final Account account, final Device device, final Envelope message, boolean online) + public void sendMessage(final Account account, final Device device, final Envelope message, final boolean online) throws NotPushRegisteredException { final String channel; @@ -83,7 +83,7 @@ public class MessageSender { if (!clientPresent) { try { - pushNotificationManager.sendNewMessageNotification(account, device.getId()); + pushNotificationManager.sendNewMessageNotification(account, device.getId(), message.getUrgent()); final boolean useVoip = StringUtils.isNotBlank(device.getVoipApnId()); RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId(), useVoip)); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotification.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotification.java index dcbaea7b1..0b2297ffa 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotification.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotification.java @@ -14,7 +14,8 @@ public record PushNotification(String deviceToken, NotificationType notificationType, @Nullable String data, @Nullable Account destination, - @Nullable Device destinationDevice) { + @Nullable Device destinationDevice, + boolean urgent) { public enum NotificationType { NOTIFICATION, CHALLENGE, RATE_LIMIT_CHALLENGE diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationManager.java index afc694b3d..89341babb 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationManager.java @@ -13,10 +13,12 @@ import io.micrometer.core.instrument.Tags; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; import org.whispersystems.textsecuregcm.redis.RedisOperation; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.util.Pair; import org.whispersystems.textsecuregcm.util.Util; @@ -27,6 +29,7 @@ public class PushNotificationManager { private final FcmSender fcmSender; private final ApnPushNotificationScheduler apnPushNotificationScheduler; private final PushLatencyManager pushLatencyManager; + private final DynamicConfigurationManager dynamicConfigurationManager; private static final String SENT_NOTIFICATION_COUNTER_NAME = name(PushNotificationManager.class, "sentPushNotification"); private static final String FAILED_NOTIFICATION_COUNTER_NAME = name(PushNotificationManager.class, "failedPushNotification"); @@ -37,25 +40,31 @@ public class PushNotificationManager { final APNSender apnSender, final FcmSender fcmSender, final ApnPushNotificationScheduler apnPushNotificationScheduler, - final PushLatencyManager pushLatencyManager) { + final PushLatencyManager pushLatencyManager, + final DynamicConfigurationManager dynamicConfigurationManager) { this.accountsManager = accountsManager; this.apnSender = apnSender; this.fcmSender = fcmSender; this.apnPushNotificationScheduler = apnPushNotificationScheduler; this.pushLatencyManager = pushLatencyManager; + this.dynamicConfigurationManager = dynamicConfigurationManager; } - public void sendNewMessageNotification(final Account destination, final long destinationDeviceId) throws NotPushRegisteredException { + public void sendNewMessageNotification(final Account destination, final long destinationDeviceId, final boolean urgent) throws NotPushRegisteredException { final Device device = destination.getDevice(destinationDeviceId).orElseThrow(NotPushRegisteredException::new); final Pair tokenAndType = getToken(device); + final boolean effectiveUrgent = + dynamicConfigurationManager.getConfiguration().getPushNotificationConfiguration().isLowUrgencyEnabled() ? + urgent : true; + sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(), - PushNotification.NotificationType.NOTIFICATION, null, destination, device)); + PushNotification.NotificationType.NOTIFICATION, null, destination, device, effectiveUrgent)); } public void sendRegistrationChallengeNotification(final String deviceToken, final PushNotification.TokenType tokenType, final String challengeToken) { - sendNotification(new PushNotification(deviceToken, tokenType, PushNotification.NotificationType.CHALLENGE, challengeToken, null, null)); + sendNotification(new PushNotification(deviceToken, tokenType, PushNotification.NotificationType.CHALLENGE, challengeToken, null, null, true)); } public void sendRateLimitChallengeNotification(final Account destination, final String challengeToken) @@ -65,7 +74,7 @@ public class PushNotificationManager { final Pair tokenAndType = getToken(device); sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(), - PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, destination, device)); + PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, destination, device, true)); } public void handleMessagesRetrieved(final Account account, final Device device, final String userAgent) { @@ -92,44 +101,55 @@ public class PushNotificationManager { @VisibleForTesting void sendNotification(final PushNotification pushNotification) { - final PushNotificationSender sender = switch (pushNotification.tokenType()) { - case FCM -> fcmSender; - case APN, APN_VOIP -> apnSender; - }; + if (pushNotification.tokenType() == PushNotification.TokenType.APN && !pushNotification.urgent()) { + // APNs imposes a per-device limit on background push notifications; schedule a notification for some time in the + // future (possibly even now!) rather than sending a notification directly + apnPushNotificationScheduler.scheduleBackgroundNotification(pushNotification.destination(), + pushNotification.destinationDevice()); + } else { + final PushNotificationSender sender = switch (pushNotification.tokenType()) { + case FCM -> fcmSender; + case APN, APN_VOIP -> apnSender; + }; - sender.sendNotification(pushNotification).whenComplete((result, throwable) -> { - if (throwable == null) { - Tags tags = Tags.of("tokenType", pushNotification.tokenType().name(), - "notificationType", pushNotification.notificationType().name(), - "accepted", String.valueOf(result.accepted()), - "unregistered", String.valueOf(result.unregistered())); + sender.sendNotification(pushNotification).whenComplete((result, throwable) -> { + if (throwable == null) { + Tags tags = Tags.of("tokenType", pushNotification.tokenType().name(), + "notificationType", pushNotification.notificationType().name(), + "urgent", String.valueOf(pushNotification.urgent()), + "accepted", String.valueOf(result.accepted()), + "unregistered", String.valueOf(result.unregistered())); - if (StringUtils.isNotBlank(result.errorCode())) { - tags = tags.and("errorCode", result.errorCode()); + if (StringUtils.isNotBlank(result.errorCode())) { + tags = tags.and("errorCode", result.errorCode()); + } + + Metrics.counter(SENT_NOTIFICATION_COUNTER_NAME, tags).increment(); + + if (result.unregistered() && pushNotification.destination() != null + && pushNotification.destinationDevice() != null) { + handleDeviceUnregistered(pushNotification.destination(), pushNotification.destinationDevice()); + } + + if (result.accepted() && + pushNotification.tokenType() == PushNotification.TokenType.APN_VOIP && + pushNotification.notificationType() == PushNotification.NotificationType.NOTIFICATION && + pushNotification.destination() != null && + pushNotification.destinationDevice() != null) { + + RedisOperation.unchecked( + () -> apnPushNotificationScheduler.scheduleRecurringVoipNotification(pushNotification.destination(), + pushNotification.destinationDevice())); + } + } else { + logger.debug("Failed to deliver {} push notification to {} ({})", + pushNotification.notificationType(), pushNotification.deviceToken(), pushNotification.tokenType(), + throwable); + + Metrics.counter(FAILED_NOTIFICATION_COUNTER_NAME, "cause", throwable.getClass().getSimpleName()).increment(); } - - Metrics.counter(SENT_NOTIFICATION_COUNTER_NAME, tags).increment(); - - if (result.unregistered() && pushNotification.destination() != null && pushNotification.destinationDevice() != null) { - handleDeviceUnregistered(pushNotification.destination(), pushNotification.destinationDevice()); - } - - if (result.accepted() && - pushNotification.tokenType() == PushNotification.TokenType.APN_VOIP && - pushNotification.notificationType() == PushNotification.NotificationType.NOTIFICATION && - pushNotification.destination() != null && - pushNotification.destinationDevice() != null) { - - RedisOperation.unchecked(() -> apnPushNotificationScheduler.scheduleRecurringVoipNotification(pushNotification.destination(), - pushNotification.destinationDevice())); - } - } else { - logger.debug("Failed to deliver {} push notification to {} ({})", - pushNotification.notificationType(), pushNotification.deviceToken(), pushNotification.tokenType(), throwable); - - Metrics.counter(FAILED_NOTIFICATION_COUNTER_NAME, "cause", throwable.getClass().getSimpleName()).increment(); - } - }); + }); + } } private void handleDeviceUnregistered(final Account account, final Device device) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/ReceiptSender.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/ReceiptSender.java index 7169c17e5..01684b624 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/ReceiptSender.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/ReceiptSender.java @@ -52,7 +52,8 @@ public class ReceiptSender { .setSourceDevice((int) sourceDeviceId) .setDestinationUuid(destinationUuid.toString()) .setTimestamp(messageId) - .setType(Envelope.Type.SERVER_DELIVERY_RECEIPT); + .setType(Envelope.Type.SERVER_DELIVERY_RECEIPT) + .setUrgent(false); return CompletableFuture.runAsync(() -> { for (final Device destinationDevice : destinationAccount.getDevices()) { diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManager.java index 0887a848c..410071fde 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/storage/ChangeNumberManager.java @@ -110,7 +110,9 @@ public class ChangeNumberManager { .setSourceUuid(sourceAndDestinationAccount.getUuid().toString()) .setSourceDevice((int) Device.MASTER_ID) .setUpdatedPni(sourceAndDestinationAccount.getPhoneNumberIdentifier().toString()) + .setUrgent(true) .build(); + messageSender.sendMessage(sourceAndDestinationAccount, destinationDevice.get(), envelope, false); } catch (NotPushRegisteredException e) { logger.debug("Not registered", e); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/websocket/AuthenticatedConnectListener.java b/service/src/main/java/org/whispersystems/textsecuregcm/websocket/AuthenticatedConnectListener.java index ec67a87e5..73e37f68b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/websocket/AuthenticatedConnectListener.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/websocket/AuthenticatedConnectListener.java @@ -96,7 +96,7 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener { if (messagesManager.hasCachedMessages(auth.getAccount().getUuid(), device.getId())) { try { - pushNotificationManager.sendNewMessageNotification(auth.getAccount(), device.getId()); + pushNotificationManager.sendNewMessageNotification(auth.getAccount(), device.getId(), true); } catch (NotPushRegisteredException ignored) { } } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/push/APNSenderTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/push/APNSenderTest.java index b3d479c7c..22d0162a5 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/push/APNSenderTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/push/APNSenderTest.java @@ -17,14 +17,18 @@ import com.eatthepath.pushy.apns.ApnsClient; import com.eatthepath.pushy.apns.ApnsPushNotification; import com.eatthepath.pushy.apns.DeliveryPriority; import com.eatthepath.pushy.apns.PushNotificationResponse; +import com.eatthepath.pushy.apns.PushType; import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification; import com.eatthepath.pushy.apns.util.concurrent.PushNotificationFuture; import java.io.IOException; import java.util.Optional; import java.util.concurrent.CompletionException; import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import org.mockito.stubbing.Answer; import org.whispersystems.textsecuregcm.storage.Account; @@ -33,45 +37,51 @@ import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService; class APNSenderTest { - private static final String DESTINATION_APN_ID = "foo"; + private static final String DESTINATION_DEVICE_TOKEN = RandomStringUtils.randomAlphanumeric(32); + private static final String BUNDLE_ID = "org.signal.test"; private Account destinationAccount; private Device destinationDevice; private ApnsClient apnsClient; + private APNSender apnSender; @BeforeEach void setup() { destinationAccount = mock(Account.class); - destinationDevice = mock(Device.class); + destinationDevice = mock(Device.class); apnsClient = mock(ApnsClient.class); + apnSender = new APNSender(new SynchronousExecutorService(), apnsClient, BUNDLE_ID); when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice)); - when(destinationDevice.getApnId()).thenReturn(DESTINATION_APN_ID); + when(destinationDevice.getApnId()).thenReturn(DESTINATION_DEVICE_TOKEN); } - @Test - void testSendVoip() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSendVoip(final boolean urgent) { PushNotificationResponse response = mock(PushNotificationResponse.class); when(response.isAccepted()).thenReturn(true); when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class))) - .thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); + .thenAnswer( + (Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); - PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice); - APNSender apnSender = new APNSender(new SynchronousExecutorService(), apnsClient, "foo"); + PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN_VOIP, + PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, urgent); final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join(); ArgumentCaptor notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class); verify(apnsClient).sendNotification(notification.capture()); - assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID); + assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_DEVICE_TOKEN); assertThat(notification.getValue().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION); assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_VOIP_NOTIFICATION_PAYLOAD); + // Delivery priority should always be `IMMEDIATE` for VOIP notifications assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); - assertThat(notification.getValue().getTopic()).isEqualTo("foo.voip"); + assertThat(notification.getValue().getTopic()).isEqualTo(BUNDLE_ID + ".voip"); assertThat(result.accepted()).isTrue(); assertThat(result.errorCode()).isNull(); @@ -80,27 +90,41 @@ class APNSenderTest { verifyNoMoreInteractions(apnsClient); } - @Test - void testSendApns() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSendApns(final boolean urgent) { PushNotificationResponse response = mock(PushNotificationResponse.class); when(response.isAccepted()).thenReturn(true); when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class))) - .thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); + .thenAnswer( + (Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); - PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice); - APNSender apnSender = new APNSender(new SynchronousExecutorService(), apnsClient, "foo"); + PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN, + PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, urgent); final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join(); ArgumentCaptor notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class); verify(apnsClient).sendNotification(notification.capture()); - assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID); + assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_DEVICE_TOKEN); assertThat(notification.getValue().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION); - assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_NSE_NOTIFICATION_PAYLOAD); - assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); - assertThat(notification.getValue().getTopic()).isEqualTo("foo"); + assertThat(notification.getValue().getPayload()) + .isEqualTo(urgent ? APNSender.APN_NSE_NOTIFICATION_PAYLOAD : APNSender.APN_BACKGROUND_PAYLOAD); + + assertThat(notification.getValue().getPriority()) + .isEqualTo(urgent ? DeliveryPriority.IMMEDIATE : DeliveryPriority.CONSERVE_POWER); + + assertThat(notification.getValue().getTopic()).isEqualTo(BUNDLE_ID); + assertThat(notification.getValue().getPushType()) + .isEqualTo(urgent ? PushType.ALERT : PushType.BACKGROUND); + + if (urgent) { + assertThat(notification.getValue().getCollapseId()).isNotNull(); + } else { + assertThat(notification.getValue().getCollapseId()).isNull(); + } assertThat(result.accepted()).isTrue(); assertThat(result.errorCode()).isNull(); @@ -110,19 +134,19 @@ class APNSenderTest { } @Test - void testUnregisteredUser() throws Exception { + void testUnregisteredUser() { PushNotificationResponse response = mock(PushNotificationResponse.class); when(response.isAccepted()).thenReturn(false); when(response.getRejectionReason()).thenReturn(Optional.of("Unregistered")); when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class))) - .thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); + .thenAnswer( + (Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); + PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN_VOIP, + PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, true); - PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice); - APNSender apnSender = new APNSender(new SynchronousExecutorService(), apnsClient, "foo"); - - when(destinationDevice.getApnId()).thenReturn(DESTINATION_APN_ID); + when(destinationDevice.getApnId()).thenReturn(DESTINATION_DEVICE_TOKEN); when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(11)); final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join(); @@ -130,7 +154,7 @@ class APNSenderTest { ArgumentCaptor notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class); verify(apnsClient).sendNotification(notification.capture()); - assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID); + assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_DEVICE_TOKEN); assertThat(notification.getValue().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION); assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_VOIP_NOTIFICATION_PAYLOAD); assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); @@ -142,24 +166,23 @@ class APNSenderTest { @Test void testGenericFailure() { - ApnsClient apnsClient = mock(ApnsClient.class); - PushNotificationResponse response = mock(PushNotificationResponse.class); when(response.isAccepted()).thenReturn(false); when(response.getRejectionReason()).thenReturn(Optional.of("BadTopic")); when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class))) - .thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); + .thenAnswer( + (Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); - PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice); - APNSender apnSender = new APNSender(new SynchronousExecutorService(), apnsClient, "foo"); + PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN_VOIP, + PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, true); final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join(); ArgumentCaptor notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class); verify(apnsClient).sendNotification(notification.capture()); - assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_APN_ID); + assertThat(notification.getValue().getToken()).isEqualTo(DESTINATION_DEVICE_TOKEN); assertThat(notification.getValue().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION); assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_VOIP_NOTIFICATION_PAYLOAD); assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE); @@ -175,10 +198,11 @@ class APNSenderTest { when(response.isAccepted()).thenReturn(true); when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class))) - .thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), new IOException("lost connection"))); + .thenAnswer((Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), + new IOException("lost connection"))); - PushNotification pushNotification = new PushNotification(DESTINATION_APN_ID, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice); - APNSender apnSender = new APNSender(new SynchronousExecutorService(), apnsClient, "foo"); + PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN_VOIP, + PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, true); assertThatThrownBy(() -> apnSender.sendNotification(pushNotification).join()) .isInstanceOf(CompletionException.class) @@ -189,7 +213,8 @@ class APNSenderTest { verifyNoMoreInteractions(apnsClient); } - private static class MockPushNotificationFuture

extends PushNotificationFuture { + private static class MockPushNotificationFuture

extends + PushNotificationFuture { MockPushNotificationFuture(final P pushNotification, final V response) { super(pushNotification); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationSchedulerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationSchedulerTest.java index 4826f2247..9a80fc4fa 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationSchedulerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/push/ApnPushNotificationSchedulerTest.java @@ -192,9 +192,7 @@ class ApnPushNotificationSchedulerTest { assertEquals(account, pushNotification.destination()); assertEquals(device, pushNotification.destinationDevice()); assertEquals(PushNotification.NotificationType.NOTIFICATION, pushNotification.notificationType()); - - // TODO Check urgency - // assertFalse(pushNotification.urgent()); + assertFalse(pushNotification.urgent()); assertEquals(0, worker.processRecurringVoipNotifications(slot)); } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/push/FcmSenderTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/push/FcmSenderTest.java index 4e174a3a5..dcc871bb2 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/push/FcmSenderTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/push/FcmSenderTest.java @@ -54,7 +54,7 @@ class FcmSenderTest { @Test void testSendMessage() { - final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null); + final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null, true); final SettableApiFuture sendFuture = SettableApiFuture.create(); sendFuture.set("message-id"); @@ -71,7 +71,7 @@ class FcmSenderTest { @Test void testSendMessageRejected() { - final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null); + final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null, true); final FirebaseMessagingException invalidArgumentException = mock(FirebaseMessagingException.class); when(invalidArgumentException.getMessagingErrorCode()).thenReturn(MessagingErrorCode.INVALID_ARGUMENT); @@ -91,7 +91,7 @@ class FcmSenderTest { @Test void testSendMessageUnregistered() { - final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null); + final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null, true); final FirebaseMessagingException unregisteredException = mock(FirebaseMessagingException.class); when(unregisteredException.getMessagingErrorCode()).thenReturn(MessagingErrorCode.UNREGISTERED); @@ -111,7 +111,7 @@ class FcmSenderTest { @Test void testSendMessageException() { - final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null); + final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null, true); final SettableApiFuture sendFuture = SettableApiFuture.create(); sendFuture.setException(new IOException()); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/push/MessageSenderTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/push/MessageSenderTest.java index 9100b4144..87cb32832 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/push/MessageSenderTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/push/MessageSenderTest.java @@ -116,7 +116,7 @@ class MessageSenderTest { messageSender.sendMessage(account, device, message, false); verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message); - verify(pushNotificationManager).sendNewMessageNotification(account, device.getId()); + verify(pushNotificationManager).sendNewMessageNotification(account, device.getId(), message.getUrgent()); } @Test @@ -127,7 +127,7 @@ class MessageSenderTest { messageSender.sendMessage(account, device, message, false); verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message); - verify(pushNotificationManager).sendNewMessageNotification(account, device.getId()); + verify(pushNotificationManager).sendNewMessageNotification(account, device.getId(), message.getUrgent()); } @Test @@ -136,7 +136,7 @@ class MessageSenderTest { when(device.getFetchesMessages()).thenReturn(true); doThrow(NotPushRegisteredException.class) - .when(pushNotificationManager).sendNewMessageNotification(account, DEVICE_ID); + .when(pushNotificationManager).sendNewMessageNotification(account, DEVICE_ID, message.getUrgent()); assertDoesNotThrow(() -> messageSender.sendMessage(account, device, message, false)); verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/push/PushNotificationManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/push/PushNotificationManagerTest.java index 8173106e9..aa18886f6 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/push/PushNotificationManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/push/PushNotificationManagerTest.java @@ -18,9 +18,14 @@ import java.util.UUID; import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration; +import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicPushNotificationConfiguration; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.AccountsManager; import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager; import org.whispersystems.textsecuregcm.tests.util.AccountsHelper; import org.whispersystems.textsecuregcm.util.Util; @@ -31,6 +36,7 @@ class PushNotificationManagerTest { private FcmSender fcmSender; private ApnPushNotificationScheduler apnPushNotificationScheduler; private PushLatencyManager pushLatencyManager; + private DynamicPushNotificationConfiguration pushNotificationConfiguration; private PushNotificationManager pushNotificationManager; @@ -41,15 +47,26 @@ class PushNotificationManagerTest { fcmSender = mock(FcmSender.class); apnPushNotificationScheduler = mock(ApnPushNotificationScheduler.class); pushLatencyManager = mock(PushLatencyManager.class); + pushNotificationConfiguration = mock(DynamicPushNotificationConfiguration.class); + + @SuppressWarnings("unchecked") final DynamicConfigurationManager dynamicConfigurationManager = + mock(DynamicConfigurationManager.class); + + final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class); + + when(dynamicConfigurationManager.getConfiguration()).thenReturn(dynamicConfiguration); + when(dynamicConfiguration.getPushNotificationConfiguration()).thenReturn(pushNotificationConfiguration); + when(pushNotificationConfiguration.isLowUrgencyEnabled()).thenReturn(true); AccountsHelper.setupMockUpdate(accountsManager); pushNotificationManager = new PushNotificationManager(accountsManager, apnSender, fcmSender, - apnPushNotificationScheduler, pushLatencyManager); + apnPushNotificationScheduler, pushLatencyManager, dynamicConfigurationManager); } - @Test - void sendNewMessageNotification() throws NotPushRegisteredException { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void sendNewMessageNotification(final boolean urgent) throws NotPushRegisteredException { final Account account = mock(Account.class); final Device device = mock(Device.class); @@ -62,8 +79,30 @@ class PushNotificationManagerTest { when(fcmSender.sendNotification(any())) .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); - pushNotificationManager.sendNewMessageNotification(account, Device.MASTER_ID); - verify(fcmSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device)); + pushNotificationManager.sendNewMessageNotification(account, Device.MASTER_ID, urgent); + verify(fcmSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent)); + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void sendNewMessageNotificationLowUrgencyDisabled(final boolean urgent) throws NotPushRegisteredException { + final Account account = mock(Account.class); + final Device device = mock(Device.class); + + final String deviceToken = "token"; + + when(device.getId()).thenReturn(Device.MASTER_ID); + when(device.getApnId()).thenReturn(deviceToken); + when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); + + when(pushNotificationConfiguration.isLowUrgencyEnabled()).thenReturn(false); + + when(apnSender.sendNotification(any())) + .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); + + pushNotificationManager.sendNewMessageNotification(account, Device.MASTER_ID, urgent); + + verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device, true)); } @Test @@ -75,7 +114,7 @@ class PushNotificationManagerTest { .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); pushNotificationManager.sendRegistrationChallengeNotification(deviceToken, PushNotification.TokenType.APN_VOIP, challengeToken); - verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.CHALLENGE, challengeToken, null, null)); + verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.CHALLENGE, challengeToken, null, null, true)); } @Test @@ -94,11 +133,12 @@ class PushNotificationManagerTest { .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); pushNotificationManager.sendRateLimitChallengeNotification(account, challengeToken); - verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN, PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, account, device)); + verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN, PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, account, device, true)); } - @Test - void testSendNotification() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSendNotificationFcm(final boolean urgent) { final Account account = mock(Account.class); final Device device = mock(Device.class); @@ -106,7 +146,7 @@ class PushNotificationManagerTest { when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); final PushNotification pushNotification = new PushNotification( - "token", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device); + "token", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent); when(fcmSender.sendNotification(pushNotification)) .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); @@ -120,8 +160,9 @@ class PushNotificationManagerTest { verifyNoInteractions(apnPushNotificationScheduler); } - @Test - void testSendNotificationApnVoip() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSendNotificationApn(final boolean urgent) { final Account account = mock(Account.class); final Device device = mock(Device.class); @@ -129,7 +170,35 @@ class PushNotificationManagerTest { when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); final PushNotification pushNotification = new PushNotification( - "token", PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device); + "token", PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent); + + when(apnSender.sendNotification(pushNotification)) + .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); + + pushNotificationManager.sendNotification(pushNotification); + + verifyNoInteractions(fcmSender); + + if (urgent) { + verify(apnSender).sendNotification(pushNotification); + verifyNoInteractions(apnPushNotificationScheduler); + } else { + verifyNoInteractions(apnSender); + verify(apnPushNotificationScheduler).scheduleBackgroundNotification(account, device); + } + } + + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void testSendNotificationApnVoip(final boolean urgent) { + final Account account = mock(Account.class); + final Device device = mock(Device.class); + + when(device.getId()).thenReturn(Device.MASTER_ID); + when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); + + final PushNotification pushNotification = new PushNotification( + "token", PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent); when(apnSender.sendNotification(pushNotification)) .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false))); @@ -137,10 +206,12 @@ class PushNotificationManagerTest { pushNotificationManager.sendNotification(pushNotification); verify(apnSender).sendNotification(pushNotification); + verifyNoInteractions(fcmSender); verify(accountsManager, never()).updateDevice(eq(account), eq(Device.MASTER_ID), any()); verify(device, never()).setUninstalledFeedbackTimestamp(Util.todayInMillis()); verify(apnPushNotificationScheduler).scheduleRecurringVoipNotification(account, device); + verify(apnPushNotificationScheduler, never()).scheduleBackgroundNotification(any(), any()); } @Test @@ -153,7 +224,7 @@ class PushNotificationManagerTest { when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); final PushNotification pushNotification = new PushNotification( - "token", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device); + "token", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device, true); when(fcmSender.sendNotification(pushNotification)) .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, null, true))); @@ -175,7 +246,7 @@ class PushNotificationManagerTest { when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device)); final PushNotification pushNotification = new PushNotification( - "token", PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device); + "token", PushNotification.TokenType.APN_VOIP, PushNotification.NotificationType.NOTIFICATION, null, account, device, true); when(apnSender.sendNotification(pushNotification)) .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, null, true)));