From 4908a0aa9e0584367549281031b9645c110639ab Mon Sep 17 00:00:00 2001 From: ravi-signal <99042880+ravi-signal@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:25:25 -0600 Subject: [PATCH] Add a 2-notification ttl=0 push notification experiment --- .../textsecuregcm/WhisperServerService.java | 23 ++++ .../ZeroTtlPushNotificationExperiment.java | 51 +++++++++ .../textsecuregcm/push/FcmSender.java | 21 +++- .../textsecuregcm/push/PushNotification.java | 9 +- .../push/PushNotificationManager.java | 18 ++- .../push/PushNotificationScheduler.java | 5 +- .../push/ZeroTtlNotificationScheduler.java | 103 ++++++++++++++++++ ...xperimentNotificationSchedulerFactory.java | 26 +++++ ...oTtlPushNotificationExperimentFactory.java | 36 ++++++ .../textsecuregcm/push/APNSenderTest.java | 12 +- .../textsecuregcm/push/FcmSenderTest.java | 8 +- .../push/PushNotificationManagerTest.java | 20 ++-- 12 files changed, 304 insertions(+), 28 deletions(-) create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/experiment/ZeroTtlPushNotificationExperiment.java create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/push/ZeroTtlNotificationScheduler.java create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/workers/ZeroTtlExperimentNotificationSchedulerFactory.java create mode 100644 service/src/main/java/org/whispersystems/textsecuregcm/workers/ZeroTtlPushNotificationExperimentFactory.java diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index c2b0bd986..ddf691585 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -268,6 +268,8 @@ import org.whispersystems.textsecuregcm.workers.BackupMetricsCommand; import org.whispersystems.textsecuregcm.workers.CertificateCommand; import org.whispersystems.textsecuregcm.workers.CheckDynamicConfigurationCommand; import org.whispersystems.textsecuregcm.workers.DeleteUserCommand; +import org.whispersystems.textsecuregcm.workers.DiscardPushNotificationExperimentSamplesCommand; +import org.whispersystems.textsecuregcm.workers.FinishPushNotificationExperimentCommand; import org.whispersystems.textsecuregcm.workers.IdleDeviceNotificationSchedulerFactory; import org.whispersystems.textsecuregcm.workers.MessagePersisterServiceCommand; import org.whispersystems.textsecuregcm.workers.NotifyIdleDevicesCommand; @@ -280,7 +282,10 @@ import org.whispersystems.textsecuregcm.workers.ScheduledApnPushNotificationSend import org.whispersystems.textsecuregcm.workers.ServerVersionCommand; import org.whispersystems.textsecuregcm.workers.SetRequestLoggingEnabledTask; import org.whispersystems.textsecuregcm.workers.SetUserDiscoverabilityCommand; +import org.whispersystems.textsecuregcm.workers.StartPushNotificationExperimentCommand; import org.whispersystems.textsecuregcm.workers.UnlinkDeviceCommand; +import org.whispersystems.textsecuregcm.workers.ZeroTtlExperimentNotificationSchedulerFactory; +import org.whispersystems.textsecuregcm.workers.ZeroTtlPushNotificationExperimentFactory; import org.whispersystems.textsecuregcm.workers.ZkParamsCommand; import org.whispersystems.websocket.WebSocketResourceProviderFactory; import org.whispersystems.websocket.setup.WebSocketEnvironment; @@ -335,6 +340,24 @@ public class WhisperServerService extends Application("start-zero-ttl-push-notification-experiment", + "Start an experiment to send push notifications with ttl=0 to idle android devices", + new ZeroTtlPushNotificationExperimentFactory())); + + bootstrap.addCommand( + new FinishPushNotificationExperimentCommand<>("finish-zero-ttl-push-notification-experiment", + "Finish an experiment to send push notifications with ttl=0 to idle android devices", + new ZeroTtlPushNotificationExperimentFactory())); + + bootstrap.addCommand( + new DiscardPushNotificationExperimentSamplesCommand("discard-zero-ttl-push-notification-experiment", + "Discard samples from the \"zero TTL push notification\" experiment", + new ZeroTtlPushNotificationExperimentFactory())); + + bootstrap.addCommand(new ProcessScheduledJobsServiceCommand("process-zero-ttl-notification-jobs", + "Processes scheduled jobs to send zero-ttl experiment notifications to idle devices", + new ZeroTtlExperimentNotificationSchedulerFactory())); + bootstrap.addCommand(new ProcessScheduledJobsServiceCommand("process-idle-device-notification-jobs", "Processes scheduled jobs to send notifications to idle devices", new IdleDeviceNotificationSchedulerFactory())); diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/experiment/ZeroTtlPushNotificationExperiment.java b/service/src/main/java/org/whispersystems/textsecuregcm/experiment/ZeroTtlPushNotificationExperiment.java new file mode 100644 index 000000000..2ca4168ea --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/experiment/ZeroTtlPushNotificationExperiment.java @@ -0,0 +1,51 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.experiment; + +import org.whispersystems.textsecuregcm.push.ZeroTtlNotificationScheduler; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.workers.IdleWakeupEligibilityChecker; +import java.time.LocalTime; +import java.util.concurrent.CompletableFuture; + +public class ZeroTtlPushNotificationExperiment extends IdleDevicePushNotificationExperiment { + private static final LocalTime PREFERRED_NOTIFICATION_TIME = LocalTime.of(14, 0); + + private final ZeroTtlNotificationScheduler zeroTtlNotificationScheduler; + + public ZeroTtlPushNotificationExperiment( + final IdleWakeupEligibilityChecker idleWakeupEligibilityChecker, + final ZeroTtlNotificationScheduler zeroTtlNotificationScheduler) { + super(idleWakeupEligibilityChecker); + this.zeroTtlNotificationScheduler = zeroTtlNotificationScheduler; + } + + @Override + boolean isIdleDeviceEligible(final Account account, final Device idleDevice, final DeviceLastSeenState state) { + return state.pushTokenType() == DeviceLastSeenState.PushTokenType.FCM; + } + + @Override + public String getExperimentName() { + return "zero-ttl-notification"; + } + + @Override + public Class getStateClass() { + return DeviceLastSeenState.class; + } + + @Override + public CompletableFuture applyExperimentTreatment(final Account account, final Device device) { + return zeroTtlNotificationScheduler.scheduleNotification(account, device, PREFERRED_NOTIFICATION_TIME, true); + } + + @Override + public CompletableFuture applyControlTreatment(final Account account, final Device device) { + return zeroTtlNotificationScheduler.scheduleNotification(account, device, PREFERRED_NOTIFICATION_TIME, false); + } +} 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 ad38ae334..8bbadafa7 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/FcmSender.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/FcmSender.java @@ -14,6 +14,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.ThreadManager; import com.google.firebase.messaging.AndroidConfig; +import com.google.firebase.messaging.AndroidNotification; import com.google.firebase.messaging.FirebaseMessaging; import com.google.firebase.messaging.FirebaseMessagingException; import com.google.firebase.messaging.Message; @@ -24,6 +25,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.ThreadFactory; @@ -78,11 +80,24 @@ public class FcmSender implements PushNotificationSender { @Override public CompletableFuture sendNotification(PushNotification pushNotification) { + final AndroidConfig.Builder androidConfig = AndroidConfig.builder() + .setPriority(pushNotification.urgent() ? AndroidConfig.Priority.HIGH : AndroidConfig.Priority.NORMAL); + + // This experiment compares the effect of two standard push notifications to one standard + one with a TTL=0 + switch (pushNotification.experimentalNotificationType().orElse(null)) { + case ZERO_TTL -> { + androidConfig.setTtl(0); + androidConfig.setCollapseKey("ttl0"); + } + case NON_COLLAPSIBLE -> + // We still want to make sure we don't collapse notifications in the control group + androidConfig.setCollapseKey("ttl0"); + case null -> {} + } + Message.Builder builder = Message.builder() .setToken(pushNotification.deviceToken()) - .setAndroidConfig(AndroidConfig.builder() - .setPriority(pushNotification.urgent() ? AndroidConfig.Priority.HIGH : AndroidConfig.Priority.NORMAL) - .build()); + .setAndroidConfig(androidConfig.build()); final String key = switch (pushNotification.notificationType()) { case NOTIFICATION -> "newMessageAlert"; 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 3f3738de5..129698e6b 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotification.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotification.java @@ -8,6 +8,7 @@ package org.whispersystems.textsecuregcm.push; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Device; import javax.annotation.Nullable; +import java.util.Optional; public record PushNotification(String deviceToken, TokenType tokenType, @@ -15,7 +16,8 @@ public record PushNotification(String deviceToken, @Nullable String data, @Nullable Account destination, @Nullable Device destinationDevice, - boolean urgent) { + boolean urgent, + Optional experimentalNotificationType) { public enum NotificationType { NOTIFICATION, @@ -28,4 +30,9 @@ public record PushNotification(String deviceToken, FCM, APN } + + public enum ExperimentalNotificationType { + ZERO_TTL, + NON_COLLAPSIBLE + } } 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 2780d78a0..04f953205 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationManager.java @@ -51,11 +51,21 @@ public class PushNotificationManager { final Pair tokenAndType = getToken(device); return sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(), - PushNotification.NotificationType.NOTIFICATION, null, destination, device, urgent)); + PushNotification.NotificationType.NOTIFICATION, null, destination, device, urgent, Optional.empty())); + } + + public CompletableFuture> sendExperimentMessageNotification( + final Account destination, final byte destinationDeviceId, final boolean urgent, PushNotification.ExperimentalNotificationType experimentalNotificationType) throws NotPushRegisteredException { + final Device device = destination.getDevice(destinationDeviceId).orElseThrow(NotPushRegisteredException::new); + final Pair tokenAndType = getToken(device); + + return sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(), + PushNotification.NotificationType.NOTIFICATION, + null, destination, device, urgent, Optional.of(experimentalNotificationType))); } public CompletableFuture sendRegistrationChallengeNotification(final String deviceToken, final PushNotification.TokenType tokenType, final String challengeToken) { - return sendNotification(new PushNotification(deviceToken, tokenType, PushNotification.NotificationType.CHALLENGE, challengeToken, null, null, true)) + return sendNotification(new PushNotification(deviceToken, tokenType, PushNotification.NotificationType.CHALLENGE, challengeToken, null, null, true, Optional.empty())) .thenApply(maybeResponse -> maybeResponse.orElseThrow(() -> new AssertionError("Responses must be present for urgent notifications"))); } @@ -66,7 +76,7 @@ public class PushNotificationManager { final Pair tokenAndType = getToken(device); return sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(), - PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, destination, device, true)) + PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, destination, device, true, Optional.empty())) .thenApply(maybeResponse -> maybeResponse.orElseThrow(() -> new AssertionError("Responses must be present for urgent notifications"))); } @@ -76,7 +86,7 @@ public class PushNotificationManager { return sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(), PushNotification.NotificationType.ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY, - context, destination, device, true)) + context, destination, device, true, Optional.empty())) .thenApply(maybeResponse -> maybeResponse.orElseThrow(() -> new AssertionError("Responses must be present for urgent notifications"))); } diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationScheduler.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationScheduler.java index 5596cba81..329c11514 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationScheduler.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/PushNotificationScheduler.java @@ -286,7 +286,7 @@ public class PushNotificationScheduler implements Managed { return pushSchedulingCluster.withCluster(connection -> connection.async().set( getLastBackgroundApnsNotificationTimestampKey(account, device), String.valueOf(clock.millis()), new SetArgs().ex(BACKGROUND_NOTIFICATION_PERIOD))) - .thenCompose(ignored -> apnSender.sendNotification(new PushNotification(device.getApnId(), PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device, false))) + .thenCompose(ignored -> apnSender.sendNotification(new PushNotification(device.getApnId(), PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device, false, Optional.empty()))) .thenAccept(response -> Metrics.counter(BACKGROUND_NOTIFICATION_SENT_COUNTER_NAME, ACCEPTED_TAG, String.valueOf(response.accepted())) .increment()) @@ -308,7 +308,8 @@ public class PushNotificationScheduler implements Managed { null, account, device, - true); + true, + Optional.empty()); final PushNotificationSender pushNotificationSender = isApnsDevice ? apnSender : fcmSender; diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/push/ZeroTtlNotificationScheduler.java b/service/src/main/java/org/whispersystems/textsecuregcm/push/ZeroTtlNotificationScheduler.java new file mode 100644 index 000000000..296bdf380 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/push/ZeroTtlNotificationScheduler.java @@ -0,0 +1,103 @@ +package org.whispersystems.textsecuregcm.push; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.annotations.VisibleForTesting; +import java.io.IOException; +import java.time.Clock; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalTime; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nullable; +import org.whispersystems.textsecuregcm.identity.IdentityType; +import org.whispersystems.textsecuregcm.scheduler.JobScheduler; +import org.whispersystems.textsecuregcm.scheduler.SchedulingUtil; +import org.whispersystems.textsecuregcm.storage.Account; +import org.whispersystems.textsecuregcm.storage.AccountsManager; +import org.whispersystems.textsecuregcm.storage.Device; +import org.whispersystems.textsecuregcm.util.SystemMapper; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; + +public class ZeroTtlNotificationScheduler extends JobScheduler { + + private final AccountsManager accountsManager; + private final PushNotificationManager pushNotificationManager; + private final Clock clock; + + @VisibleForTesting + record JobDescriptor(UUID accountIdentifier, byte deviceId, long lastSeen, boolean zeroTtl) {} + + public ZeroTtlNotificationScheduler( + final AccountsManager accountsManager, + final PushNotificationManager pushNotificationManager, + final DynamoDbAsyncClient dynamoDbAsyncClient, + final String tableName, + final Duration jobExpiration, + final Clock clock) { + + super(dynamoDbAsyncClient, tableName, jobExpiration, clock); + + this.accountsManager = accountsManager; + this.pushNotificationManager = pushNotificationManager; + this.clock = clock; + } + + @Override + public String getSchedulerName() { + return "ZeroTtlNotification"; + } + + @Override + protected CompletableFuture processJob(@Nullable final byte[] jobData) { + final JobDescriptor jobDescriptor; + + try { + jobDescriptor = SystemMapper.jsonMapper().readValue(jobData, JobDescriptor.class); + } catch (final IOException e) { + return CompletableFuture.failedFuture(e); + } + + return accountsManager.getByAccountIdentifierAsync(jobDescriptor.accountIdentifier()) + .thenCompose(maybeAccount -> maybeAccount.map(account -> + account.getDevice(jobDescriptor.deviceId()).map(device -> { + if (jobDescriptor.lastSeen() != device.getLastSeen()) { + return CompletableFuture.completedFuture("deviceSeenRecently"); + } + + try { + return sendNotification(account, jobDescriptor).thenApply(ignored -> "sent"); + } catch (final NotPushRegisteredException e) { + return CompletableFuture.completedFuture("deviceTokenDeleted"); + } + }) + .orElse(CompletableFuture.completedFuture("deviceDeleted"))) + .orElse(CompletableFuture.completedFuture("accountDeleted"))); + } + + private CompletableFuture sendNotification(final Account account, + final JobDescriptor jobDescriptor) throws NotPushRegisteredException { + final CompletableFuture> standardNotification = pushNotificationManager.sendNewMessageNotification( + account, jobDescriptor.deviceId(), true); + final CompletableFuture> experimentNotification = pushNotificationManager.sendExperimentMessageNotification( + account, jobDescriptor.deviceId(), true, jobDescriptor.zeroTtl() + ? PushNotification.ExperimentalNotificationType.ZERO_TTL + : PushNotification.ExperimentalNotificationType.NON_COLLAPSIBLE); + + return standardNotification.thenCombine(experimentNotification, (ignored1, ignored2) -> null); + } + + public CompletableFuture scheduleNotification(final Account account, final Device device, + final LocalTime preferredDeliveryTime, boolean zeroTtl) { + final Instant runAt = SchedulingUtil.getNextRecommendedNotificationTime(account, preferredDeliveryTime, clock); + + try { + return scheduleJob(runAt, SystemMapper.jsonMapper().writeValueAsBytes( + new JobDescriptor(account.getIdentifier(IdentityType.ACI), device.getId(), device.getLastSeen(), zeroTtl))); + } catch (final JsonProcessingException e) { + // This should never happen when serializing an `AccountAndDeviceIdentifier` + throw new AssertionError(e); + } + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/ZeroTtlExperimentNotificationSchedulerFactory.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/ZeroTtlExperimentNotificationSchedulerFactory.java new file mode 100644 index 000000000..6439374b6 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/workers/ZeroTtlExperimentNotificationSchedulerFactory.java @@ -0,0 +1,26 @@ +package org.whispersystems.textsecuregcm.workers; + +import java.time.Clock; +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.configuration.DynamoDbTables; +import org.whispersystems.textsecuregcm.push.ZeroTtlNotificationScheduler; +import org.whispersystems.textsecuregcm.scheduler.JobScheduler; + +public class ZeroTtlExperimentNotificationSchedulerFactory implements JobSchedulerFactory { + + @Override + public JobScheduler buildJobScheduler(final CommandDependencies commandDependencies, + final WhisperServerConfiguration configuration) { + final DynamoDbTables.TableWithExpiration tableConfiguration = configuration.getDynamoDbTables().getScheduledJobs(); + + final Clock clock = Clock.systemUTC(); + + return new ZeroTtlNotificationScheduler( + commandDependencies.accountsManager(), + commandDependencies.pushNotificationManager(), + commandDependencies.dynamoDbAsyncClient(), + tableConfiguration.getTableName(), + tableConfiguration.getExpiration(), + clock); + } +} diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/workers/ZeroTtlPushNotificationExperimentFactory.java b/service/src/main/java/org/whispersystems/textsecuregcm/workers/ZeroTtlPushNotificationExperimentFactory.java new file mode 100644 index 000000000..1134203d0 --- /dev/null +++ b/service/src/main/java/org/whispersystems/textsecuregcm/workers/ZeroTtlPushNotificationExperimentFactory.java @@ -0,0 +1,36 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.workers; + +import org.whispersystems.textsecuregcm.WhisperServerConfiguration; +import org.whispersystems.textsecuregcm.configuration.DynamoDbTables; +import org.whispersystems.textsecuregcm.experiment.DeviceLastSeenState; +import org.whispersystems.textsecuregcm.experiment.PushNotificationExperiment; +import org.whispersystems.textsecuregcm.experiment.ZeroTtlPushNotificationExperiment; +import org.whispersystems.textsecuregcm.push.ZeroTtlNotificationScheduler; +import java.time.Clock; + +public class ZeroTtlPushNotificationExperimentFactory implements PushNotificationExperimentFactory { + + @Override + public PushNotificationExperiment buildExperiment(final CommandDependencies commandDependencies, + final WhisperServerConfiguration configuration) { + + final DynamoDbTables.TableWithExpiration tableConfiguration = configuration.getDynamoDbTables().getScheduledJobs(); + + final Clock clock = Clock.systemUTC(); + + return new ZeroTtlPushNotificationExperiment( + new IdleWakeupEligibilityChecker(clock, commandDependencies.messagesManager()), + new ZeroTtlNotificationScheduler( + commandDependencies.accountsManager(), + commandDependencies.pushNotificationManager(), + commandDependencies.dynamoDbAsyncClient(), + tableConfiguration.getTableName(), + tableConfiguration.getExpiration(), + clock)); + } +} 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 250ff3285..05c634875 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/push/APNSenderTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/push/APNSenderTest.java @@ -69,7 +69,8 @@ class APNSenderTest { (Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN, - PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, urgent); + PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, urgent, + Optional.empty()); final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join(); @@ -113,7 +114,8 @@ class APNSenderTest { (Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN, - PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, true); + PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, true, + Optional.empty()); when(destinationDevice.getApnId()).thenReturn(DESTINATION_DEVICE_TOKEN); when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(11)); @@ -144,7 +146,8 @@ class APNSenderTest { (Answer) invocationOnMock -> new MockPushNotificationFuture<>(invocationOnMock.getArgument(0), response)); PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN, - PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, true); + PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, true, + Optional.empty()); final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join(); @@ -171,7 +174,8 @@ class APNSenderTest { new IOException("lost connection"))); PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN, - PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, true); + PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, true, + Optional.empty()); assertThatThrownBy(() -> apnSender.sendNotification(pushNotification).join()) .isInstanceOf(CompletionException.class) 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 c6b5088c9..8244b7488 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, true); + final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null, true, Optional.empty()); 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, true); + final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null, true, Optional.empty()); 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, true); + final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null, true, Optional.empty()); 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, true); + final PushNotification pushNotification = new PushNotification("foo", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, null, null, true, Optional.empty()); final SettableApiFuture sendFuture = SettableApiFuture.create(); sendFuture.setException(new IOException()); 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 c1153d6e4..b955dd498 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/push/PushNotificationManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/push/PushNotificationManagerTest.java @@ -66,7 +66,7 @@ class PushNotificationManagerTest { .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, Optional.empty(), false, Optional.empty()))); pushNotificationManager.sendNewMessageNotification(account, Device.PRIMARY_ID, urgent); - verify(fcmSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent)); + verify(fcmSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent, Optional.empty())); } @Test @@ -78,7 +78,7 @@ class PushNotificationManagerTest { .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, Optional.empty(), false, Optional.empty()))); pushNotificationManager.sendRegistrationChallengeNotification(deviceToken, PushNotification.TokenType.APN, challengeToken); - verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN, PushNotification.NotificationType.CHALLENGE, challengeToken, null, null, true)); + verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN, PushNotification.NotificationType.CHALLENGE, challengeToken, null, null, true, Optional.empty())); } @Test @@ -97,7 +97,7 @@ class PushNotificationManagerTest { .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, Optional.empty(), false, Optional.empty()))); pushNotificationManager.sendRateLimitChallengeNotification(account, challengeToken); - verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN, PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, account, device, true)); + verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN, PushNotification.NotificationType.RATE_LIMIT_CHALLENGE, challengeToken, account, device, true, Optional.empty())); } @ParameterizedTest @@ -124,10 +124,10 @@ class PushNotificationManagerTest { if (isApn){ verify(apnSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.APN, - PushNotification.NotificationType.ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY, "someContext", account, device, true)); + PushNotification.NotificationType.ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY, "someContext", account, device, true, Optional.empty())); } else { verify(fcmSender, times(1)).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.FCM, - PushNotification.NotificationType.ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY, "someContext", account, device, true)); + PushNotification.NotificationType.ATTEMPT_LOGIN_NOTIFICATION_HIGH_PRIORITY, "someContext", account, device, true, Optional.empty())); } } @@ -141,7 +141,7 @@ class PushNotificationManagerTest { when(account.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(device)); final PushNotification pushNotification = new PushNotification( - "token", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent); + "token", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent, Optional.empty()); when(fcmSender.sendNotification(pushNotification)) .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, Optional.empty(), false, Optional.empty()))); @@ -165,7 +165,7 @@ class PushNotificationManagerTest { when(account.getDevice(Device.PRIMARY_ID)).thenReturn(Optional.of(device)); final PushNotification pushNotification = new PushNotification( - "token", PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent); + "token", PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device, urgent, Optional.empty()); when(apnSender.sendNotification(pushNotification)) .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, Optional.empty(), false, Optional.empty()))); @@ -200,7 +200,7 @@ class PushNotificationManagerTest { when(accountsManager.getByAccountIdentifier(aci)).thenReturn(Optional.of(account)); final PushNotification pushNotification = new PushNotification( - "token", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device, true); + "token", PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device, true, Optional.empty()); when(fcmSender.sendNotification(pushNotification)) .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, Optional.empty(), true, Optional.empty()))); @@ -225,7 +225,7 @@ class PushNotificationManagerTest { when(accountsManager.getByAccountIdentifier(aci)).thenReturn(Optional.of(account)); final PushNotification pushNotification = new PushNotification( - "token", PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device, true); + "token", PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device, true, Optional.empty()); when(apnSender.sendNotification(pushNotification)) .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, Optional.empty(), true, Optional.empty()))); @@ -256,7 +256,7 @@ class PushNotificationManagerTest { when(accountsManager.getByAccountIdentifier(aci)).thenReturn(Optional.of(account)); final PushNotification pushNotification = new PushNotification( - "token", PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device, true); + "token", PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device, true, Optional.empty()); when(apnSender.sendNotification(pushNotification)) .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, Optional.empty(), true, Optional.of(tokenTimestamp.minusSeconds(60)))));