This repository has been archived by the owner on Apr 17, 2023. It is now read-only.
/
APNsPushNotificationSender.java
301 lines (261 loc) · 13.8 KB
/
APNsPushNotificationSender.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
/**
* 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 static org.jboss.aerogear.unifiedpush.message.util.ConfigurationUtils.tryGetIntegerProperty;
import static org.jboss.aerogear.unifiedpush.message.util.ConfigurationUtils.tryGetProperty;
import java.io.ByteArrayInputStream;
import java.util.Collection;
import java.util.Date;
import javax.inject.Inject;
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.cache.AbstractServiceCache.ServiceConstructor;
import org.jboss.aerogear.unifiedpush.message.cache.ApnsServiceCache;
import org.jboss.aerogear.unifiedpush.message.exception.PushNetworkUnreachableException;
import org.jboss.aerogear.unifiedpush.message.exception.SenderResourceNotAvailableException;
import org.jboss.aerogear.unifiedpush.service.ClientInstallationService;
import org.jboss.aerogear.unifiedpush.utils.AeroGearLogger;
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;
@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 ApnsServiceCache apnsServiceCache;
public APNsPushNotificationSender() {
}
/**
* Constructor used for test purposes
*/
APNsPushNotificationSender(ApnsServiceCache apnsServiceCache) {
this.apnsServiceCache = apnsServiceCache;
}
/**
* 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<String> 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
.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
// was a badge included?
if (message.getBadge() >= 0) {
builder.badge(message.getBadge()); // only set badge if needed
}
//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();
ApnsService service = apnsServiceCache.dequeueOrCreateNewService(pushMessageInformationId, iOSVariant.getVariantID(), new ServiceConstructor<ApnsService>() {
@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("One batch to APNs has been submitted");
apnsServiceCache.queueFreedUpService(pushMessageInformationId, iOSVariant.getVariantID(), service);
try {
service = null; // we don't want a failure in onSuccess stop the APNs service
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 {
apnsServiceCache.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(System.currentTimeMillis() + EnhancedApnsNotification.MAXIMUM_EXPIRY * 1000L);
} else {
// apply the given TTL to the current time
return new Date(System.currentTimeMillis() + ttl);
}
}
/**
* 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 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);
}
}
}