org.jboss.aerogear.unifiedpush.message.sender.APNsPushNotificationSender Maven / Gradle / Ivy
/**
* JBoss, Home of Professional Open Source
* Copyright Red Hat, Inc., and individual contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jboss.aerogear.unifiedpush.message.sender;
import com.notnoop.apns.APNS;
import com.notnoop.apns.ApnsDelegateAdapter;
import com.notnoop.apns.ApnsNotification;
import com.notnoop.apns.ApnsService;
import com.notnoop.apns.ApnsServiceBuilder;
import com.notnoop.apns.DeliveryError;
import com.notnoop.apns.EnhancedApnsNotification;
import com.notnoop.apns.PayloadBuilder;
import com.notnoop.apns.internal.Utilities;
import com.notnoop.exceptions.ApnsDeliveryErrorException;
import org.jboss.aerogear.unifiedpush.api.Variant;
import org.jboss.aerogear.unifiedpush.api.VariantType;
import org.jboss.aerogear.unifiedpush.api.iOSVariant;
import org.jboss.aerogear.unifiedpush.message.InternalUnifiedPushMessage;
import org.jboss.aerogear.unifiedpush.message.Message;
import org.jboss.aerogear.unifiedpush.message.UnifiedPushMessage;
import org.jboss.aerogear.unifiedpush.message.apns.APNs;
import org.jboss.aerogear.unifiedpush.message.exception.PushNetworkUnreachableException;
import org.jboss.aerogear.unifiedpush.message.exception.SenderResourceNotAvailableException;
import org.jboss.aerogear.unifiedpush.message.serviceHolder.ApnsServiceHolder;
import org.jboss.aerogear.unifiedpush.message.serviceHolder.ServiceConstructor;
import org.jboss.aerogear.unifiedpush.message.serviceHolder.ServiceDestroyer;
import org.jboss.aerogear.unifiedpush.service.ClientInstallationService;
import org.jboss.aerogear.unifiedpush.utils.AeroGearLogger;
import javax.inject.Inject;
import java.io.ByteArrayInputStream;
import java.util.Collection;
import java.util.Date;
import static org.jboss.aerogear.unifiedpush.system.ConfigurationUtils.tryGetIntegerProperty;
import static org.jboss.aerogear.unifiedpush.system.ConfigurationUtils.tryGetProperty;
@SenderType(VariantType.IOS)
public class APNsPushNotificationSender implements PushNotificationSender {
public static final String CUSTOM_AEROGEAR_APNS_PUSH_HOST = "custom.aerogear.apns.push.host";
public static final String CUSTOM_AEROGEAR_APNS_PUSH_PORT = "custom.aerogear.apns.push.port";
private static final String CUSTOM_AEROGEAR_APNS_FEEDBACK_HOST = "custom.aerogear.apns.feedback.host";
private static final String CUSTOM_AEROGEAR_APNS_FEEDBACK_PORT = "custom.aerogear.apns.feedback.port";
private static final String customAerogearApnsPushHost = tryGetProperty(CUSTOM_AEROGEAR_APNS_PUSH_HOST);
private static final Integer customAerogearApnsPushPort = tryGetIntegerProperty(CUSTOM_AEROGEAR_APNS_PUSH_PORT);
private static final String customAerogearApnsFeedbackHost = tryGetProperty(CUSTOM_AEROGEAR_APNS_FEEDBACK_HOST);
private static final Integer customAerogearApnsFeedbackPort = tryGetIntegerProperty(CUSTOM_AEROGEAR_APNS_FEEDBACK_PORT);
private final AeroGearLogger logger = AeroGearLogger.getInstance(APNsPushNotificationSender.class);
@Inject
private ClientInstallationService clientInstallationService;
@Inject
private ApnsServiceHolder apnsServiceHolder;
public APNsPushNotificationSender() {
}
/**
* Constructor used for test purposes
*/
APNsPushNotificationSender(ApnsServiceHolder apnsServiceHolder) {
this.apnsServiceHolder = apnsServiceHolder;
}
/**
* Sends APNs notifications ({@link UnifiedPushMessage}) to all devices, that are represented by
* the {@link Collection} of tokens for the given {@link iOSVariant}.
*
* @param variant contains details for the underlying push network, e.g. API Keys/Ids
* @param tokens contains the list of tokens that identifies the installation to which the message will be sent
* @param pushMessage payload to be send to the given clients
* @param pushMessageInformationId the id of the PushMessageInformation instance associated with this send.
* @param callback that will be invoked after the sending.
*/
public void sendPushMessage(final Variant variant, final Collection tokens, final UnifiedPushMessage pushMessage, final String pushMessageInformationId, final NotificationSenderCallback callback) {
// no need to send empty list
if (tokens.isEmpty()) {
return;
}
final iOSVariant iOSVariant = (iOSVariant) variant;
Message message = pushMessage.getMessage();
APNs apns = message.getApns();
PayloadBuilder builder = APNS.newPayload()
// adding recognized key values
.alertBody(message.getAlert()) // alert dialog, in iOS or Safari
.badge(message.getBadge()) // little badge icon update;
.sound(message.getSound()) // sound to be played by app
.alertTitle(apns.getTitle()) // The title of the notification in Safari and Apple Watch
.alertAction(apns.getAction()) // The label of the action button, if the user sets the notifications to appear as alerts in Safari.
.urlArgs(apns.getUrlArgs())
.category(apns.getActionCategory()) // iOS8: User Action category
.localizedTitleKey(apns.getLocalizedTitleKey()); //iOS8 : Localized Title Key
//this kind of check should belong in java-apns
if(apns.getLocalizedTitleArguments() != null) {
builder .localizedArguments(apns.getLocalizedTitleArguments()); //iOS8 : Localized Title Arguments;
}
// apply the 'content-available:1' value:
if (apns.isContentAvailable()) {
// content-available is for 'silent' notifications and Newsstand
builder = builder.instantDeliveryOrSilentNotification();
}
builder = builder.customFields(message.getUserData()); // adding other (submitted) fields
//add aerogear-push-id
builder = builder.customField(InternalUnifiedPushMessage.PUSH_MESSAGE_ID, pushMessageInformationId);
// we are done with adding values here, before building let's check if the msg is too long
if (builder.isTooLong()) {
// invoke the error callback and return, as it is pointless to send something out
callback.onError("Nothing sent to APNs since the payload is too large");
return;
}
// all good, let's build the JSON payload for APNs
final String apnsMessage = builder.build();
final ApnsService service = apnsServiceHolder.dequeueOrCreateNewService(pushMessageInformationId, iOSVariant.getVariantID(), new ServiceConstructor() {
@Override
public ApnsService construct() {
ApnsService service = buildApnsService(iOSVariant, callback);
if (service == null) {
callback.onError("No certificate was found. Could not send messages to APNs");
throw new IllegalStateException("No certificate was found. Could not send messages to APNs");
} else {
logger.fine("Starting APNs service");
try {
service.start();
} catch (Exception e) {
throw new PushNetworkUnreachableException(e);
}
return service;
}
}
});
if (service == null) {
throw new SenderResourceNotAvailableException("Unable to obtain a ApnsService instance");
}
try {
logger.fine("Sending transformed APNs payload: " + apnsMessage);
Date expireDate = createFutureDateBasedOnTTL(pushMessage.getConfig().getTimeToLive());
service.push(tokens, apnsMessage, expireDate);
logger.info(String.format("Sent push notification to the Apple APNs Server for %d tokens",tokens.size()));
apnsServiceHolder.queueFreedUpService(pushMessageInformationId, iOSVariant.getVariantID(), service, new ServiceDestroyer() {
@Override
public void destroy(ApnsService instance) {
service.stop();
}
});
try {
callback.onSuccess();
} catch (Exception e) {
logger.severe("Failed to call onSuccess after successful push", e);
}
} catch (Exception e) {
try {
logger.warning("APNs service died in the middle of sending, stopping it");
try {
service.stop();
} catch (Exception ex) {
logger.severe("Failed to stop the APNs service after failure", ex);
}
callback.onError("Error sending payload to APNs server: " + e.getMessage());
} finally {
apnsServiceHolder.freeUpSlot(pushMessageInformationId, iOSVariant.getVariantID());
}
}
}
/**
* Helper method that creates a future {@link Date}, based on the given ttl/time-to-live value.
* If no TTL was provided, we use the max date from the APNs library
*/
private Date createFutureDateBasedOnTTL(int ttl) {
// no TTL was specified on the payload, we use the MAX Default from the APNs library:
if (ttl == -1) {
return new Date(EnhancedApnsNotification.MAXIMUM_EXPIRY * 1000L);
} else {
// apply the given TTL to the current time
return new Date(System.currentTimeMillis() + ttl * 1000L);
}
}
/**
* Returns the ApnsService, based on the required profile (production VS sandbox/test).
* Null is returned if there is no "configuration" for the request stage
*/
private ApnsService buildApnsService(final iOSVariant iOSVariant, final NotificationSenderCallback notificationSenderCallback) {
// this check should not be needed, but you never know:
if (iOSVariant.getCertificate() != null && iOSVariant.getPassphrase() != null) {
final ApnsServiceBuilder builder = APNS.newService();
// using the APNS Delegate callback to log success/failure for each token:
builder.withDelegate(new ApnsDelegateAdapter() {
@Override
public void messageSent(ApnsNotification message, boolean resent) {
// Invoked for EVERY devicetoken:
logger.finest("Sending APNs message to: " + message.getDeviceToken());
}
@Override
public void messageSendFailed(ApnsNotification message, Throwable e) {
if (e.getClass().isAssignableFrom(ApnsDeliveryErrorException.class)) {
ApnsDeliveryErrorException deliveryError = (ApnsDeliveryErrorException) e;
if (DeliveryError.INVALID_TOKEN.equals(deliveryError.getDeliveryError())) {
final String invalidToken = Utilities.encodeHex(message.getDeviceToken()).toLowerCase();
logger.info("Removing invalid (not allowed) token: " + invalidToken);
clientInstallationService.removeInstallationForVariantByDeviceToken(iOSVariant.getVariantID(), invalidToken);
} else {
// for now, we just log the other cases
logger.severe("Error sending payload to APNs server", e);
}
}
}
});
// add the certificate:
try {
ByteArrayInputStream stream = new ByteArrayInputStream(iOSVariant.getCertificate());
builder.withCert(stream, iOSVariant.getPassphrase());
// release the stream
stream.close();
} catch (Exception e) {
logger.severe("Error reading certificate", e);
// indicating an incomplete service
return null;
}
configureDestinations(iOSVariant, builder);
// create the service
return builder.build();
}
// null if, why ever, there was no cert/passphrase
return null;
}
/**
* Configure the Gateway to the Apns servers.
* Default gateway and port can be override with respectively :
* - custom.aerogear.apns.push.host
* - custom.aerogear.apns.push.port
*
* Feedback gateway and port can be override with respectively :
* - custom.aerogear.apns.feedback.host
* - custom.aerogear.apns.feedback.port
* @param iOSVariant
* @param builder
*/
private void configureDestinations(iOSVariant iOSVariant, ApnsServiceBuilder builder) {
// pick the destination, based on submitted profile:
builder.withAppleDestination(iOSVariant.isProduction());
//Is the gateway host&port provided by a system property, for tests ?
if(customAerogearApnsPushHost != null){
int port = Utilities.SANDBOX_GATEWAY_PORT;
if(customAerogearApnsPushPort != null) {
port = customAerogearApnsPushPort;
}
builder.withGatewayDestination(customAerogearApnsPushHost, port);
}
//Is the feedback gateway provided by a system property, for tests ?
if(customAerogearApnsFeedbackHost != null){
int port = Utilities.SANDBOX_FEEDBACK_PORT;
if(customAerogearApnsFeedbackPort != null) {
port = customAerogearApnsFeedbackPort;
}
builder.withFeedbackDestination(customAerogearApnsFeedbackHost, port);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy