Send non-urgent push notifications with lower priority
This commit is contained in:
		
							parent
							
								
									5f6b66dad6
								
							
						
					
					
						commit
						b4281c5a70
					
				| 
						 | 
					@ -455,7 +455,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
 | 
				
			||||||
    APNSender                  apnSender                  = new APNSender(apnSenderExecutor, config.getApnConfiguration());
 | 
					    APNSender                  apnSender                  = new APNSender(apnSenderExecutor, config.getApnConfiguration());
 | 
				
			||||||
    FcmSender                  fcmSender                  = new FcmSender(fcmSenderExecutor, config.getFcmConfiguration().credentials());
 | 
					    FcmSender                  fcmSender                  = new FcmSender(fcmSenderExecutor, config.getFcmConfiguration().credentials());
 | 
				
			||||||
    ApnPushNotificationScheduler apnPushNotificationScheduler = new ApnPushNotificationScheduler(pushSchedulerCluster, apnSender, accountsManager);
 | 
					    ApnPushNotificationScheduler apnPushNotificationScheduler = new ApnPushNotificationScheduler(pushSchedulerCluster, apnSender, accountsManager);
 | 
				
			||||||
    PushNotificationManager    pushNotificationManager    = new PushNotificationManager(accountsManager, apnSender, fcmSender, apnPushNotificationScheduler, pushLatencyManager);
 | 
					    PushNotificationManager    pushNotificationManager    = new PushNotificationManager(accountsManager, apnSender, fcmSender, apnPushNotificationScheduler, pushLatencyManager, dynamicConfigurationManager);
 | 
				
			||||||
    RateLimiters               rateLimiters               = new RateLimiters(config.getLimitsConfiguration(), rateLimitersCluster);
 | 
					    RateLimiters               rateLimiters               = new RateLimiters(config.getLimitsConfiguration(), rateLimitersCluster);
 | 
				
			||||||
    DynamicRateLimiters        dynamicRateLimiters        = new DynamicRateLimiters(rateLimitersCluster, dynamicConfigurationManager);
 | 
					    DynamicRateLimiters        dynamicRateLimiters        = new DynamicRateLimiters(rateLimitersCluster, dynamicConfigurationManager);
 | 
				
			||||||
    ProvisioningManager        provisioningManager        = new ProvisioningManager(pubSubManager);
 | 
					    ProvisioningManager        provisioningManager        = new ProvisioningManager(pubSubManager);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -64,6 +64,10 @@ public class DynamicConfiguration {
 | 
				
			||||||
  @Valid
 | 
					  @Valid
 | 
				
			||||||
  DynamicMessagePersisterConfiguration messagePersister = new DynamicMessagePersisterConfiguration();
 | 
					  DynamicMessagePersisterConfiguration messagePersister = new DynamicMessagePersisterConfiguration();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @JsonProperty
 | 
				
			||||||
 | 
					  @Valid
 | 
				
			||||||
 | 
					  DynamicPushNotificationConfiguration pushNotifications = new DynamicPushNotificationConfiguration();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
 | 
					  public Optional<DynamicExperimentEnrollmentConfiguration> getExperimentEnrollmentConfiguration(
 | 
				
			||||||
      final String experimentName) {
 | 
					      final String experimentName) {
 | 
				
			||||||
    return Optional.ofNullable(experiments.get(experimentName));
 | 
					    return Optional.ofNullable(experiments.get(experimentName));
 | 
				
			||||||
| 
						 | 
					@ -126,4 +130,8 @@ public class DynamicConfiguration {
 | 
				
			||||||
  public DynamicMessagePersisterConfiguration getMessagePersisterConfiguration() {
 | 
					  public DynamicMessagePersisterConfiguration getMessagePersisterConfiguration() {
 | 
				
			||||||
    return messagePersister;
 | 
					    return messagePersister;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public DynamicPushNotificationConfiguration getPushNotificationConfiguration() {
 | 
				
			||||||
 | 
					    return pushNotifications;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -45,6 +45,11 @@ public class APNSender implements Managed, PushNotificationSender {
 | 
				
			||||||
      .setLocalizedAlertMessage("APN_Message")
 | 
					      .setLocalizedAlertMessage("APN_Message")
 | 
				
			||||||
      .build();
 | 
					      .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @VisibleForTesting
 | 
				
			||||||
 | 
					  static final String APN_BACKGROUND_PAYLOAD = new SimpleApnsPayloadBuilder()
 | 
				
			||||||
 | 
					      .setContentAvailable(true)
 | 
				
			||||||
 | 
					      .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @VisibleForTesting
 | 
					  @VisibleForTesting
 | 
				
			||||||
  static final Instant MAX_EXPIRATION = Instant.ofEpochMilli(Integer.MAX_VALUE * 1000L);
 | 
					  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 boolean isVoip = notification.tokenType() == PushNotification.TokenType.APN_VOIP;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final String payload = switch (notification.notificationType()) {
 | 
					    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()
 | 
					      case CHALLENGE -> new SimpleApnsPayloadBuilder()
 | 
				
			||||||
          .setSound("default")
 | 
					          .setSound("default")
 | 
				
			||||||
| 
						 | 
					@ -98,8 +109,19 @@ public class APNSender implements Managed, PushNotificationSender {
 | 
				
			||||||
          .build();
 | 
					          .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 =
 | 
					    final String collapseId =
 | 
				
			||||||
        (notification.notificationType() == PushNotification.NotificationType.NOTIFICATION && !isVoip)
 | 
					        (notification.notificationType() == PushNotification.NotificationType.NOTIFICATION && notification.urgent() && !isVoip)
 | 
				
			||||||
            ? "incoming-message" : null;
 | 
					            ? "incoming-message" : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final Instant start = Instant.now();
 | 
					    final Instant start = Instant.now();
 | 
				
			||||||
| 
						 | 
					@ -108,8 +130,8 @@ public class APNSender implements Managed, PushNotificationSender {
 | 
				
			||||||
        topic,
 | 
					        topic,
 | 
				
			||||||
        payload,
 | 
					        payload,
 | 
				
			||||||
        MAX_EXPIRATION,
 | 
					        MAX_EXPIRATION,
 | 
				
			||||||
        DeliveryPriority.IMMEDIATE,
 | 
					        deliveryPriority,
 | 
				
			||||||
        isVoip ? PushType.VOIP : PushType.ALERT,
 | 
					        pushType,
 | 
				
			||||||
        collapseId))
 | 
					        collapseId))
 | 
				
			||||||
        .whenComplete((response, throwable) -> {
 | 
					        .whenComplete((response, throwable) -> {
 | 
				
			||||||
          // Note that we deliberately run this small bit of non-blocking measurement on the "send notification" thread
 | 
					          // Note that we deliberately run this small bit of non-blocking measurement on the "send notification" thread
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -250,7 +250,7 @@ public class ApnPushNotificationScheduler implements Managed {
 | 
				
			||||||
      return;
 | 
					      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();
 | 
					    retry.increment();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -263,8 +263,7 @@ public class ApnPushNotificationScheduler implements Managed {
 | 
				
			||||||
          getLastBackgroundNotificationTimestampKey(account, device),
 | 
					          getLastBackgroundNotificationTimestampKey(account, device),
 | 
				
			||||||
          String.valueOf(clock.millis()), new SetArgs().ex(BACKGROUND_NOTIFICATION_PERIOD)));
 | 
					          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, false));
 | 
				
			||||||
      apnSender.sendNotification(new PushNotification(device.getApnId(), PushNotification.TokenType.APN, PushNotification.NotificationType.NOTIFICATION, null, account, device));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      backgroundNotificationSentCounter.increment();
 | 
					      backgroundNotificationSentCounter.increment();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -84,7 +84,7 @@ public class FcmSender implements PushNotificationSender {
 | 
				
			||||||
    Message.Builder builder = Message.builder()
 | 
					    Message.Builder builder = Message.builder()
 | 
				
			||||||
        .setToken(pushNotification.deviceToken())
 | 
					        .setToken(pushNotification.deviceToken())
 | 
				
			||||||
        .setAndroidConfig(AndroidConfig.builder()
 | 
					        .setAndroidConfig(AndroidConfig.builder()
 | 
				
			||||||
            .setPriority(AndroidConfig.Priority.HIGH)
 | 
					            .setPriority(pushNotification.urgent() ? AndroidConfig.Priority.HIGH : AndroidConfig.Priority.NORMAL)
 | 
				
			||||||
            .build());
 | 
					            .build());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final String key = switch (pushNotification.notificationType()) {
 | 
					    final String key = switch (pushNotification.notificationType()) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -50,7 +50,7 @@ public class MessageSender {
 | 
				
			||||||
    this.pushLatencyManager = pushLatencyManager;
 | 
					    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 {
 | 
					      throws NotPushRegisteredException {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final String channel;
 | 
					    final String channel;
 | 
				
			||||||
| 
						 | 
					@ -83,7 +83,7 @@ public class MessageSender {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!clientPresent) {
 | 
					      if (!clientPresent) {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
          pushNotificationManager.sendNewMessageNotification(account, device.getId());
 | 
					          pushNotificationManager.sendNewMessageNotification(account, device.getId(), message.getUrgent());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          final boolean useVoip = StringUtils.isNotBlank(device.getVoipApnId());
 | 
					          final boolean useVoip = StringUtils.isNotBlank(device.getVoipApnId());
 | 
				
			||||||
          RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId(), useVoip));
 | 
					          RedisOperation.unchecked(() -> pushLatencyManager.recordPushSent(account.getUuid(), device.getId(), useVoip));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,7 +14,8 @@ public record PushNotification(String deviceToken,
 | 
				
			||||||
                               NotificationType notificationType,
 | 
					                               NotificationType notificationType,
 | 
				
			||||||
                               @Nullable String data,
 | 
					                               @Nullable String data,
 | 
				
			||||||
                               @Nullable Account destination,
 | 
					                               @Nullable Account destination,
 | 
				
			||||||
                               @Nullable Device destinationDevice) {
 | 
					                               @Nullable Device destinationDevice,
 | 
				
			||||||
 | 
					                               boolean urgent) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public enum NotificationType {
 | 
					  public enum NotificationType {
 | 
				
			||||||
    NOTIFICATION, CHALLENGE, RATE_LIMIT_CHALLENGE
 | 
					    NOTIFICATION, CHALLENGE, RATE_LIMIT_CHALLENGE
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,10 +13,12 @@ import io.micrometer.core.instrument.Tags;
 | 
				
			||||||
import org.apache.commons.lang3.StringUtils;
 | 
					import org.apache.commons.lang3.StringUtils;
 | 
				
			||||||
import org.slf4j.Logger;
 | 
					import org.slf4j.Logger;
 | 
				
			||||||
import org.slf4j.LoggerFactory;
 | 
					import org.slf4j.LoggerFactory;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.redis.RedisOperation;
 | 
					import org.whispersystems.textsecuregcm.redis.RedisOperation;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Account;
 | 
					import org.whispersystems.textsecuregcm.storage.Account;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
 | 
					import org.whispersystems.textsecuregcm.storage.AccountsManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Device;
 | 
					import org.whispersystems.textsecuregcm.storage.Device;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.util.Pair;
 | 
					import org.whispersystems.textsecuregcm.util.Pair;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.util.Util;
 | 
					import org.whispersystems.textsecuregcm.util.Util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,6 +29,7 @@ public class PushNotificationManager {
 | 
				
			||||||
  private final FcmSender fcmSender;
 | 
					  private final FcmSender fcmSender;
 | 
				
			||||||
  private final ApnPushNotificationScheduler apnPushNotificationScheduler;
 | 
					  private final ApnPushNotificationScheduler apnPushNotificationScheduler;
 | 
				
			||||||
  private final PushLatencyManager pushLatencyManager;
 | 
					  private final PushLatencyManager pushLatencyManager;
 | 
				
			||||||
 | 
					  private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static final String SENT_NOTIFICATION_COUNTER_NAME = name(PushNotificationManager.class, "sentPushNotification");
 | 
					  private static final String SENT_NOTIFICATION_COUNTER_NAME = name(PushNotificationManager.class, "sentPushNotification");
 | 
				
			||||||
  private static final String FAILED_NOTIFICATION_COUNTER_NAME = name(PushNotificationManager.class, "failedPushNotification");
 | 
					  private static final String FAILED_NOTIFICATION_COUNTER_NAME = name(PushNotificationManager.class, "failedPushNotification");
 | 
				
			||||||
| 
						 | 
					@ -37,25 +40,31 @@ public class PushNotificationManager {
 | 
				
			||||||
      final APNSender apnSender,
 | 
					      final APNSender apnSender,
 | 
				
			||||||
      final FcmSender fcmSender,
 | 
					      final FcmSender fcmSender,
 | 
				
			||||||
      final ApnPushNotificationScheduler apnPushNotificationScheduler,
 | 
					      final ApnPushNotificationScheduler apnPushNotificationScheduler,
 | 
				
			||||||
      final PushLatencyManager pushLatencyManager) {
 | 
					      final PushLatencyManager pushLatencyManager,
 | 
				
			||||||
 | 
					      final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.accountsManager = accountsManager;
 | 
					    this.accountsManager = accountsManager;
 | 
				
			||||||
    this.apnSender = apnSender;
 | 
					    this.apnSender = apnSender;
 | 
				
			||||||
    this.fcmSender = fcmSender;
 | 
					    this.fcmSender = fcmSender;
 | 
				
			||||||
    this.apnPushNotificationScheduler = apnPushNotificationScheduler;
 | 
					    this.apnPushNotificationScheduler = apnPushNotificationScheduler;
 | 
				
			||||||
    this.pushLatencyManager = pushLatencyManager;
 | 
					    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 Device device = destination.getDevice(destinationDeviceId).orElseThrow(NotPushRegisteredException::new);
 | 
				
			||||||
    final Pair<String, PushNotification.TokenType> tokenAndType = getToken(device);
 | 
					    final Pair<String, PushNotification.TokenType> tokenAndType = getToken(device);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final boolean effectiveUrgent =
 | 
				
			||||||
 | 
					        dynamicConfigurationManager.getConfiguration().getPushNotificationConfiguration().isLowUrgencyEnabled() ?
 | 
				
			||||||
 | 
					            urgent : true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(),
 | 
					    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) {
 | 
					  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)
 | 
					  public void sendRateLimitChallengeNotification(final Account destination, final String challengeToken)
 | 
				
			||||||
| 
						 | 
					@ -65,7 +74,7 @@ public class PushNotificationManager {
 | 
				
			||||||
    final Pair<String, PushNotification.TokenType> tokenAndType = getToken(device);
 | 
					    final Pair<String, PushNotification.TokenType> tokenAndType = getToken(device);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sendNotification(new PushNotification(tokenAndType.first(), tokenAndType.second(),
 | 
					    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) {
 | 
					  public void handleMessagesRetrieved(final Account account, final Device device, final String userAgent) {
 | 
				
			||||||
| 
						 | 
					@ -92,44 +101,55 @@ public class PushNotificationManager {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @VisibleForTesting
 | 
					  @VisibleForTesting
 | 
				
			||||||
  void sendNotification(final PushNotification pushNotification) {
 | 
					  void sendNotification(final PushNotification pushNotification) {
 | 
				
			||||||
    final PushNotificationSender sender = switch (pushNotification.tokenType()) {
 | 
					    if (pushNotification.tokenType() == PushNotification.TokenType.APN && !pushNotification.urgent()) {
 | 
				
			||||||
      case FCM -> fcmSender;
 | 
					      // APNs imposes a per-device limit on background push notifications; schedule a notification for some time in the
 | 
				
			||||||
      case APN, APN_VOIP -> apnSender;
 | 
					      // 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) -> {
 | 
					      sender.sendNotification(pushNotification).whenComplete((result, throwable) -> {
 | 
				
			||||||
      if (throwable == null) {
 | 
					        if (throwable == null) {
 | 
				
			||||||
        Tags tags = Tags.of("tokenType", pushNotification.tokenType().name(),
 | 
					          Tags tags = Tags.of("tokenType", pushNotification.tokenType().name(),
 | 
				
			||||||
            "notificationType", pushNotification.notificationType().name(),
 | 
					              "notificationType", pushNotification.notificationType().name(),
 | 
				
			||||||
            "accepted", String.valueOf(result.accepted()),
 | 
					              "urgent", String.valueOf(pushNotification.urgent()),
 | 
				
			||||||
            "unregistered", String.valueOf(result.unregistered()));
 | 
					              "accepted", String.valueOf(result.accepted()),
 | 
				
			||||||
 | 
					              "unregistered", String.valueOf(result.unregistered()));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (StringUtils.isNotBlank(result.errorCode())) {
 | 
					          if (StringUtils.isNotBlank(result.errorCode())) {
 | 
				
			||||||
          tags = tags.and("errorCode", 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) {
 | 
					  private void handleDeviceUnregistered(final Account account, final Device device) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -52,7 +52,8 @@ public class ReceiptSender {
 | 
				
			||||||
        .setSourceDevice((int) sourceDeviceId)
 | 
					        .setSourceDevice((int) sourceDeviceId)
 | 
				
			||||||
        .setDestinationUuid(destinationUuid.toString())
 | 
					        .setDestinationUuid(destinationUuid.toString())
 | 
				
			||||||
        .setTimestamp(messageId)
 | 
					        .setTimestamp(messageId)
 | 
				
			||||||
        .setType(Envelope.Type.SERVER_DELIVERY_RECEIPT);
 | 
					        .setType(Envelope.Type.SERVER_DELIVERY_RECEIPT)
 | 
				
			||||||
 | 
					        .setUrgent(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return CompletableFuture.runAsync(() -> {
 | 
					    return CompletableFuture.runAsync(() -> {
 | 
				
			||||||
      for (final Device destinationDevice : destinationAccount.getDevices()) {
 | 
					      for (final Device destinationDevice : destinationAccount.getDevices()) {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -110,7 +110,9 @@ public class ChangeNumberManager {
 | 
				
			||||||
          .setSourceUuid(sourceAndDestinationAccount.getUuid().toString())
 | 
					          .setSourceUuid(sourceAndDestinationAccount.getUuid().toString())
 | 
				
			||||||
          .setSourceDevice((int) Device.MASTER_ID)
 | 
					          .setSourceDevice((int) Device.MASTER_ID)
 | 
				
			||||||
          .setUpdatedPni(sourceAndDestinationAccount.getPhoneNumberIdentifier().toString())
 | 
					          .setUpdatedPni(sourceAndDestinationAccount.getPhoneNumberIdentifier().toString())
 | 
				
			||||||
 | 
					          .setUrgent(true)
 | 
				
			||||||
          .build();
 | 
					          .build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      messageSender.sendMessage(sourceAndDestinationAccount, destinationDevice.get(), envelope, false);
 | 
					      messageSender.sendMessage(sourceAndDestinationAccount, destinationDevice.get(), envelope, false);
 | 
				
			||||||
    } catch (NotPushRegisteredException e) {
 | 
					    } catch (NotPushRegisteredException e) {
 | 
				
			||||||
      logger.debug("Not registered", e);
 | 
					      logger.debug("Not registered", e);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -96,7 +96,7 @@ public class AuthenticatedConnectListener implements WebSocketConnectListener {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (messagesManager.hasCachedMessages(auth.getAccount().getUuid(), device.getId())) {
 | 
					            if (messagesManager.hasCachedMessages(auth.getAccount().getUuid(), device.getId())) {
 | 
				
			||||||
              try {
 | 
					              try {
 | 
				
			||||||
                pushNotificationManager.sendNewMessageNotification(auth.getAccount(), device.getId());
 | 
					                pushNotificationManager.sendNewMessageNotification(auth.getAccount(), device.getId(), true);
 | 
				
			||||||
              } catch (NotPushRegisteredException ignored) {
 | 
					              } catch (NotPushRegisteredException ignored) {
 | 
				
			||||||
              }
 | 
					              }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,14 +17,18 @@ import com.eatthepath.pushy.apns.ApnsClient;
 | 
				
			||||||
import com.eatthepath.pushy.apns.ApnsPushNotification;
 | 
					import com.eatthepath.pushy.apns.ApnsPushNotification;
 | 
				
			||||||
import com.eatthepath.pushy.apns.DeliveryPriority;
 | 
					import com.eatthepath.pushy.apns.DeliveryPriority;
 | 
				
			||||||
import com.eatthepath.pushy.apns.PushNotificationResponse;
 | 
					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.SimpleApnsPushNotification;
 | 
				
			||||||
import com.eatthepath.pushy.apns.util.concurrent.PushNotificationFuture;
 | 
					import com.eatthepath.pushy.apns.util.concurrent.PushNotificationFuture;
 | 
				
			||||||
import java.io.IOException;
 | 
					import java.io.IOException;
 | 
				
			||||||
import java.util.Optional;
 | 
					import java.util.Optional;
 | 
				
			||||||
import java.util.concurrent.CompletionException;
 | 
					import java.util.concurrent.CompletionException;
 | 
				
			||||||
import java.util.concurrent.TimeUnit;
 | 
					import java.util.concurrent.TimeUnit;
 | 
				
			||||||
 | 
					import org.apache.commons.lang3.RandomStringUtils;
 | 
				
			||||||
import org.junit.jupiter.api.BeforeEach;
 | 
					import org.junit.jupiter.api.BeforeEach;
 | 
				
			||||||
import org.junit.jupiter.api.Test;
 | 
					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.ArgumentCaptor;
 | 
				
			||||||
import org.mockito.stubbing.Answer;
 | 
					import org.mockito.stubbing.Answer;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Account;
 | 
					import org.whispersystems.textsecuregcm.storage.Account;
 | 
				
			||||||
| 
						 | 
					@ -33,45 +37,51 @@ import org.whispersystems.textsecuregcm.tests.util.SynchronousExecutorService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class APNSenderTest {
 | 
					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 Account destinationAccount;
 | 
				
			||||||
  private Device destinationDevice;
 | 
					  private Device destinationDevice;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private ApnsClient apnsClient;
 | 
					  private ApnsClient apnsClient;
 | 
				
			||||||
 | 
					  private APNSender apnSender;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @BeforeEach
 | 
					  @BeforeEach
 | 
				
			||||||
  void setup() {
 | 
					  void setup() {
 | 
				
			||||||
    destinationAccount = mock(Account.class);
 | 
					    destinationAccount = mock(Account.class);
 | 
				
			||||||
    destinationDevice  = mock(Device.class);
 | 
					    destinationDevice = mock(Device.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    apnsClient = mock(ApnsClient.class);
 | 
					    apnsClient = mock(ApnsClient.class);
 | 
				
			||||||
 | 
					    apnSender = new APNSender(new SynchronousExecutorService(), apnsClient, BUNDLE_ID);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice));
 | 
					    when(destinationAccount.getDevice(1)).thenReturn(Optional.of(destinationDevice));
 | 
				
			||||||
    when(destinationDevice.getApnId()).thenReturn(DESTINATION_APN_ID);
 | 
					    when(destinationDevice.getApnId()).thenReturn(DESTINATION_DEVICE_TOKEN);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @ParameterizedTest
 | 
				
			||||||
  void testSendVoip() {
 | 
					  @ValueSource(booleans = {true, false})
 | 
				
			||||||
 | 
					  void testSendVoip(final boolean urgent) {
 | 
				
			||||||
    PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
 | 
					    PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
 | 
				
			||||||
    when(response.isAccepted()).thenReturn(true);
 | 
					    when(response.isAccepted()).thenReturn(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
 | 
					    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);
 | 
					    PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN_VOIP,
 | 
				
			||||||
    APNSender          apnSender          = new APNSender(new SynchronousExecutorService(), apnsClient, "foo");
 | 
					        PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, urgent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
 | 
					    final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
 | 
					    ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
 | 
				
			||||||
    verify(apnsClient).sendNotification(notification.capture());
 | 
					    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().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION);
 | 
				
			||||||
    assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_VOIP_NOTIFICATION_PAYLOAD);
 | 
					    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().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.accepted()).isTrue();
 | 
				
			||||||
    assertThat(result.errorCode()).isNull();
 | 
					    assertThat(result.errorCode()).isNull();
 | 
				
			||||||
| 
						 | 
					@ -80,27 +90,41 @@ class APNSenderTest {
 | 
				
			||||||
    verifyNoMoreInteractions(apnsClient);
 | 
					    verifyNoMoreInteractions(apnsClient);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @ParameterizedTest
 | 
				
			||||||
  void testSendApns() {
 | 
					  @ValueSource(booleans = {true, false})
 | 
				
			||||||
 | 
					  void testSendApns(final boolean urgent) {
 | 
				
			||||||
    PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
 | 
					    PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
 | 
				
			||||||
    when(response.isAccepted()).thenReturn(true);
 | 
					    when(response.isAccepted()).thenReturn(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
 | 
					    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);
 | 
					    PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN,
 | 
				
			||||||
    APNSender          apnSender          = new APNSender(new SynchronousExecutorService(), apnsClient, "foo");
 | 
					        PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, urgent);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
 | 
					    final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
 | 
					    ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
 | 
				
			||||||
    verify(apnsClient).sendNotification(notification.capture());
 | 
					    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().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION);
 | 
				
			||||||
    assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_NSE_NOTIFICATION_PAYLOAD);
 | 
					    assertThat(notification.getValue().getPayload())
 | 
				
			||||||
    assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
 | 
					        .isEqualTo(urgent ? APNSender.APN_NSE_NOTIFICATION_PAYLOAD : APNSender.APN_BACKGROUND_PAYLOAD);
 | 
				
			||||||
    assertThat(notification.getValue().getTopic()).isEqualTo("foo");
 | 
					
 | 
				
			||||||
 | 
					    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.accepted()).isTrue();
 | 
				
			||||||
    assertThat(result.errorCode()).isNull();
 | 
					    assertThat(result.errorCode()).isNull();
 | 
				
			||||||
| 
						 | 
					@ -110,19 +134,19 @@ class APNSenderTest {
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void testUnregisteredUser() throws Exception {
 | 
					  void testUnregisteredUser() {
 | 
				
			||||||
    PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
 | 
					    PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
 | 
				
			||||||
    when(response.isAccepted()).thenReturn(false);
 | 
					    when(response.isAccepted()).thenReturn(false);
 | 
				
			||||||
    when(response.getRejectionReason()).thenReturn(Optional.of("Unregistered"));
 | 
					    when(response.getRejectionReason()).thenReturn(Optional.of("Unregistered"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
 | 
					    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);
 | 
					    when(destinationDevice.getApnId()).thenReturn(DESTINATION_DEVICE_TOKEN);
 | 
				
			||||||
    APNSender          apnSender          = new APNSender(new SynchronousExecutorService(), apnsClient, "foo");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    when(destinationDevice.getApnId()).thenReturn(DESTINATION_APN_ID);
 | 
					 | 
				
			||||||
    when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(11));
 | 
					    when(destinationDevice.getPushTimestamp()).thenReturn(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(11));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
 | 
					    final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
 | 
				
			||||||
| 
						 | 
					@ -130,7 +154,7 @@ class APNSenderTest {
 | 
				
			||||||
    ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
 | 
					    ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
 | 
				
			||||||
    verify(apnsClient).sendNotification(notification.capture());
 | 
					    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().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION);
 | 
				
			||||||
    assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_VOIP_NOTIFICATION_PAYLOAD);
 | 
					    assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_VOIP_NOTIFICATION_PAYLOAD);
 | 
				
			||||||
    assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
 | 
					    assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
 | 
				
			||||||
| 
						 | 
					@ -142,24 +166,23 @@ class APNSenderTest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void testGenericFailure() {
 | 
					  void testGenericFailure() {
 | 
				
			||||||
    ApnsClient      apnsClient      = mock(ApnsClient.class);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
 | 
					    PushNotificationResponse<SimpleApnsPushNotification> response = mock(PushNotificationResponse.class);
 | 
				
			||||||
    when(response.isAccepted()).thenReturn(false);
 | 
					    when(response.isAccepted()).thenReturn(false);
 | 
				
			||||||
    when(response.getRejectionReason()).thenReturn(Optional.of("BadTopic"));
 | 
					    when(response.getRejectionReason()).thenReturn(Optional.of("BadTopic"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
 | 
					    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);
 | 
					    PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN_VOIP,
 | 
				
			||||||
    APNSender          apnSender          = new APNSender(new SynchronousExecutorService(), apnsClient, "foo");
 | 
					        PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
 | 
					    final SendPushNotificationResult result = apnSender.sendNotification(pushNotification).join();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
 | 
					    ArgumentCaptor<SimpleApnsPushNotification> notification = ArgumentCaptor.forClass(SimpleApnsPushNotification.class);
 | 
				
			||||||
    verify(apnsClient).sendNotification(notification.capture());
 | 
					    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().getExpiration()).isEqualTo(APNSender.MAX_EXPIRATION);
 | 
				
			||||||
    assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_VOIP_NOTIFICATION_PAYLOAD);
 | 
					    assertThat(notification.getValue().getPayload()).isEqualTo(APNSender.APN_VOIP_NOTIFICATION_PAYLOAD);
 | 
				
			||||||
    assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
 | 
					    assertThat(notification.getValue().getPriority()).isEqualTo(DeliveryPriority.IMMEDIATE);
 | 
				
			||||||
| 
						 | 
					@ -175,10 +198,11 @@ class APNSenderTest {
 | 
				
			||||||
    when(response.isAccepted()).thenReturn(true);
 | 
					    when(response.isAccepted()).thenReturn(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    when(apnsClient.sendNotification(any(SimpleApnsPushNotification.class)))
 | 
					    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);
 | 
					    PushNotification pushNotification = new PushNotification(DESTINATION_DEVICE_TOKEN, PushNotification.TokenType.APN_VOIP,
 | 
				
			||||||
    APNSender          apnSender          = new APNSender(new SynchronousExecutorService(), apnsClient, "foo");
 | 
					        PushNotification.NotificationType.NOTIFICATION, null, destinationAccount, destinationDevice, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assertThatThrownBy(() -> apnSender.sendNotification(pushNotification).join())
 | 
					    assertThatThrownBy(() -> apnSender.sendNotification(pushNotification).join())
 | 
				
			||||||
        .isInstanceOf(CompletionException.class)
 | 
					        .isInstanceOf(CompletionException.class)
 | 
				
			||||||
| 
						 | 
					@ -189,7 +213,8 @@ class APNSenderTest {
 | 
				
			||||||
    verifyNoMoreInteractions(apnsClient);
 | 
					    verifyNoMoreInteractions(apnsClient);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static class MockPushNotificationFuture <P extends ApnsPushNotification, V> extends PushNotificationFuture<P, V> {
 | 
					  private static class MockPushNotificationFuture<P extends ApnsPushNotification, V> extends
 | 
				
			||||||
 | 
					      PushNotificationFuture<P, V> {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    MockPushNotificationFuture(final P pushNotification, final V response) {
 | 
					    MockPushNotificationFuture(final P pushNotification, final V response) {
 | 
				
			||||||
      super(pushNotification);
 | 
					      super(pushNotification);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -192,9 +192,7 @@ class ApnPushNotificationSchedulerTest {
 | 
				
			||||||
    assertEquals(account, pushNotification.destination());
 | 
					    assertEquals(account, pushNotification.destination());
 | 
				
			||||||
    assertEquals(device, pushNotification.destinationDevice());
 | 
					    assertEquals(device, pushNotification.destinationDevice());
 | 
				
			||||||
    assertEquals(PushNotification.NotificationType.NOTIFICATION, pushNotification.notificationType());
 | 
					    assertEquals(PushNotification.NotificationType.NOTIFICATION, pushNotification.notificationType());
 | 
				
			||||||
 | 
					    assertFalse(pushNotification.urgent());
 | 
				
			||||||
    // TODO Check urgency
 | 
					 | 
				
			||||||
    // assertFalse(pushNotification.urgent());
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assertEquals(0, worker.processRecurringVoipNotifications(slot));
 | 
					    assertEquals(0, worker.processRecurringVoipNotifications(slot));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -54,7 +54,7 @@ class FcmSenderTest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void testSendMessage() {
 | 
					  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<String> sendFuture = SettableApiFuture.create();
 | 
					    final SettableApiFuture<String> sendFuture = SettableApiFuture.create();
 | 
				
			||||||
    sendFuture.set("message-id");
 | 
					    sendFuture.set("message-id");
 | 
				
			||||||
| 
						 | 
					@ -71,7 +71,7 @@ class FcmSenderTest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void testSendMessageRejected() {
 | 
					  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);
 | 
					    final FirebaseMessagingException invalidArgumentException = mock(FirebaseMessagingException.class);
 | 
				
			||||||
    when(invalidArgumentException.getMessagingErrorCode()).thenReturn(MessagingErrorCode.INVALID_ARGUMENT);
 | 
					    when(invalidArgumentException.getMessagingErrorCode()).thenReturn(MessagingErrorCode.INVALID_ARGUMENT);
 | 
				
			||||||
| 
						 | 
					@ -91,7 +91,7 @@ class FcmSenderTest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void testSendMessageUnregistered() {
 | 
					  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);
 | 
					    final FirebaseMessagingException unregisteredException = mock(FirebaseMessagingException.class);
 | 
				
			||||||
    when(unregisteredException.getMessagingErrorCode()).thenReturn(MessagingErrorCode.UNREGISTERED);
 | 
					    when(unregisteredException.getMessagingErrorCode()).thenReturn(MessagingErrorCode.UNREGISTERED);
 | 
				
			||||||
| 
						 | 
					@ -111,7 +111,7 @@ class FcmSenderTest {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
  void testSendMessageException() {
 | 
					  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<String> sendFuture = SettableApiFuture.create();
 | 
					    final SettableApiFuture<String> sendFuture = SettableApiFuture.create();
 | 
				
			||||||
    sendFuture.setException(new IOException());
 | 
					    sendFuture.setException(new IOException());
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -116,7 +116,7 @@ class MessageSenderTest {
 | 
				
			||||||
    messageSender.sendMessage(account, device, message, false);
 | 
					    messageSender.sendMessage(account, device, message, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message);
 | 
					    verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message);
 | 
				
			||||||
    verify(pushNotificationManager).sendNewMessageNotification(account, device.getId());
 | 
					    verify(pushNotificationManager).sendNewMessageNotification(account, device.getId(), message.getUrgent());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
| 
						 | 
					@ -127,7 +127,7 @@ class MessageSenderTest {
 | 
				
			||||||
    messageSender.sendMessage(account, device, message, false);
 | 
					    messageSender.sendMessage(account, device, message, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message);
 | 
					    verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message);
 | 
				
			||||||
    verify(pushNotificationManager).sendNewMessageNotification(account, device.getId());
 | 
					    verify(pushNotificationManager).sendNewMessageNotification(account, device.getId(), message.getUrgent());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
| 
						 | 
					@ -136,7 +136,7 @@ class MessageSenderTest {
 | 
				
			||||||
    when(device.getFetchesMessages()).thenReturn(true);
 | 
					    when(device.getFetchesMessages()).thenReturn(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    doThrow(NotPushRegisteredException.class)
 | 
					    doThrow(NotPushRegisteredException.class)
 | 
				
			||||||
        .when(pushNotificationManager).sendNewMessageNotification(account, DEVICE_ID);
 | 
					        .when(pushNotificationManager).sendNewMessageNotification(account, DEVICE_ID, message.getUrgent());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    assertDoesNotThrow(() -> messageSender.sendMessage(account, device, message, false));
 | 
					    assertDoesNotThrow(() -> messageSender.sendMessage(account, device, message, false));
 | 
				
			||||||
    verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message);
 | 
					    verify(messagesManager).insert(ACCOUNT_UUID, DEVICE_ID, message);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,9 +18,14 @@ import java.util.UUID;
 | 
				
			||||||
import java.util.concurrent.CompletableFuture;
 | 
					import java.util.concurrent.CompletableFuture;
 | 
				
			||||||
import org.junit.jupiter.api.BeforeEach;
 | 
					import org.junit.jupiter.api.BeforeEach;
 | 
				
			||||||
import org.junit.jupiter.api.Test;
 | 
					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.Account;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.AccountsManager;
 | 
					import org.whispersystems.textsecuregcm.storage.AccountsManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.storage.Device;
 | 
					import org.whispersystems.textsecuregcm.storage.Device;
 | 
				
			||||||
 | 
					import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
 | 
					import org.whispersystems.textsecuregcm.tests.util.AccountsHelper;
 | 
				
			||||||
import org.whispersystems.textsecuregcm.util.Util;
 | 
					import org.whispersystems.textsecuregcm.util.Util;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,6 +36,7 @@ class PushNotificationManagerTest {
 | 
				
			||||||
  private FcmSender fcmSender;
 | 
					  private FcmSender fcmSender;
 | 
				
			||||||
  private ApnPushNotificationScheduler apnPushNotificationScheduler;
 | 
					  private ApnPushNotificationScheduler apnPushNotificationScheduler;
 | 
				
			||||||
  private PushLatencyManager pushLatencyManager;
 | 
					  private PushLatencyManager pushLatencyManager;
 | 
				
			||||||
 | 
					  private DynamicPushNotificationConfiguration pushNotificationConfiguration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private PushNotificationManager pushNotificationManager;
 | 
					  private PushNotificationManager pushNotificationManager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,15 +47,26 @@ class PushNotificationManagerTest {
 | 
				
			||||||
    fcmSender = mock(FcmSender.class);
 | 
					    fcmSender = mock(FcmSender.class);
 | 
				
			||||||
    apnPushNotificationScheduler = mock(ApnPushNotificationScheduler.class);
 | 
					    apnPushNotificationScheduler = mock(ApnPushNotificationScheduler.class);
 | 
				
			||||||
    pushLatencyManager = mock(PushLatencyManager.class);
 | 
					    pushLatencyManager = mock(PushLatencyManager.class);
 | 
				
			||||||
 | 
					    pushNotificationConfiguration = mock(DynamicPushNotificationConfiguration.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @SuppressWarnings("unchecked") final DynamicConfigurationManager<DynamicConfiguration> 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);
 | 
					    AccountsHelper.setupMockUpdate(accountsManager);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pushNotificationManager = new PushNotificationManager(accountsManager, apnSender, fcmSender,
 | 
					    pushNotificationManager = new PushNotificationManager(accountsManager, apnSender, fcmSender,
 | 
				
			||||||
        apnPushNotificationScheduler, pushLatencyManager);
 | 
					        apnPushNotificationScheduler, pushLatencyManager, dynamicConfigurationManager);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @ParameterizedTest
 | 
				
			||||||
  void sendNewMessageNotification() throws NotPushRegisteredException {
 | 
					  @ValueSource(booleans = {true, false})
 | 
				
			||||||
 | 
					  void sendNewMessageNotification(final boolean urgent) throws NotPushRegisteredException {
 | 
				
			||||||
    final Account account = mock(Account.class);
 | 
					    final Account account = mock(Account.class);
 | 
				
			||||||
    final Device device = mock(Device.class);
 | 
					    final Device device = mock(Device.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -62,8 +79,30 @@ class PushNotificationManagerTest {
 | 
				
			||||||
    when(fcmSender.sendNotification(any()))
 | 
					    when(fcmSender.sendNotification(any()))
 | 
				
			||||||
        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
 | 
					        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pushNotificationManager.sendNewMessageNotification(account, Device.MASTER_ID);
 | 
					    pushNotificationManager.sendNewMessageNotification(account, Device.MASTER_ID, urgent);
 | 
				
			||||||
    verify(fcmSender).sendNotification(new PushNotification(deviceToken, PushNotification.TokenType.FCM, PushNotification.NotificationType.NOTIFICATION, null, account, device));
 | 
					    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
 | 
					  @Test
 | 
				
			||||||
| 
						 | 
					@ -75,7 +114,7 @@ class PushNotificationManagerTest {
 | 
				
			||||||
        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
 | 
					        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pushNotificationManager.sendRegistrationChallengeNotification(deviceToken, PushNotification.TokenType.APN_VOIP, challengeToken);
 | 
					    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
 | 
					  @Test
 | 
				
			||||||
| 
						 | 
					@ -94,11 +133,12 @@ class PushNotificationManagerTest {
 | 
				
			||||||
        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
 | 
					        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pushNotificationManager.sendRateLimitChallengeNotification(account, challengeToken);
 | 
					    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
 | 
					  @ParameterizedTest
 | 
				
			||||||
  void testSendNotification() {
 | 
					  @ValueSource(booleans = {true, false})
 | 
				
			||||||
 | 
					  void testSendNotificationFcm(final boolean urgent) {
 | 
				
			||||||
    final Account account = mock(Account.class);
 | 
					    final Account account = mock(Account.class);
 | 
				
			||||||
    final Device device = mock(Device.class);
 | 
					    final Device device = mock(Device.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -106,7 +146,7 @@ class PushNotificationManagerTest {
 | 
				
			||||||
    when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device));
 | 
					    when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final PushNotification pushNotification = new PushNotification(
 | 
					    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))
 | 
					    when(fcmSender.sendNotification(pushNotification))
 | 
				
			||||||
        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
 | 
					        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
 | 
				
			||||||
| 
						 | 
					@ -120,8 +160,9 @@ class PushNotificationManagerTest {
 | 
				
			||||||
    verifyNoInteractions(apnPushNotificationScheduler);
 | 
					    verifyNoInteractions(apnPushNotificationScheduler);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @ParameterizedTest
 | 
				
			||||||
  void testSendNotificationApnVoip() {
 | 
					  @ValueSource(booleans = {true, false})
 | 
				
			||||||
 | 
					  void testSendNotificationApn(final boolean urgent) {
 | 
				
			||||||
    final Account account = mock(Account.class);
 | 
					    final Account account = mock(Account.class);
 | 
				
			||||||
    final Device device = mock(Device.class);
 | 
					    final Device device = mock(Device.class);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -129,7 +170,35 @@ class PushNotificationManagerTest {
 | 
				
			||||||
    when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device));
 | 
					    when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final PushNotification pushNotification = new PushNotification(
 | 
					    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))
 | 
					    when(apnSender.sendNotification(pushNotification))
 | 
				
			||||||
        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
 | 
					        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(true, null, false)));
 | 
				
			||||||
| 
						 | 
					@ -137,10 +206,12 @@ class PushNotificationManagerTest {
 | 
				
			||||||
    pushNotificationManager.sendNotification(pushNotification);
 | 
					    pushNotificationManager.sendNotification(pushNotification);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    verify(apnSender).sendNotification(pushNotification);
 | 
					    verify(apnSender).sendNotification(pushNotification);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    verifyNoInteractions(fcmSender);
 | 
					    verifyNoInteractions(fcmSender);
 | 
				
			||||||
    verify(accountsManager, never()).updateDevice(eq(account), eq(Device.MASTER_ID), any());
 | 
					    verify(accountsManager, never()).updateDevice(eq(account), eq(Device.MASTER_ID), any());
 | 
				
			||||||
    verify(device, never()).setUninstalledFeedbackTimestamp(Util.todayInMillis());
 | 
					    verify(device, never()).setUninstalledFeedbackTimestamp(Util.todayInMillis());
 | 
				
			||||||
    verify(apnPushNotificationScheduler).scheduleRecurringVoipNotification(account, device);
 | 
					    verify(apnPushNotificationScheduler).scheduleRecurringVoipNotification(account, device);
 | 
				
			||||||
 | 
					    verify(apnPushNotificationScheduler, never()).scheduleBackgroundNotification(any(), any());
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Test
 | 
					  @Test
 | 
				
			||||||
| 
						 | 
					@ -153,7 +224,7 @@ class PushNotificationManagerTest {
 | 
				
			||||||
    when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device));
 | 
					    when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final PushNotification pushNotification = new PushNotification(
 | 
					    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))
 | 
					    when(fcmSender.sendNotification(pushNotification))
 | 
				
			||||||
        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, null, true)));
 | 
					        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, null, true)));
 | 
				
			||||||
| 
						 | 
					@ -175,7 +246,7 @@ class PushNotificationManagerTest {
 | 
				
			||||||
    when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device));
 | 
					    when(account.getDevice(Device.MASTER_ID)).thenReturn(Optional.of(device));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    final PushNotification pushNotification = new PushNotification(
 | 
					    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))
 | 
					    when(apnSender.sendNotification(pushNotification))
 | 
				
			||||||
        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, null, true)));
 | 
					        .thenReturn(CompletableFuture.completedFuture(new SendPushNotificationResult(false, null, true)));
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue