/**
* 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.google.android.gcm.server.Constants;
import com.google.android.gcm.server.Message;
import com.google.android.gcm.server.Message.Builder;
import com.google.android.gcm.server.MulticastResult;
import com.google.android.gcm.server.Result;
import com.google.android.gcm.server.Sender;
import org.jboss.aerogear.unifiedpush.api.AndroidVariant;
import org.jboss.aerogear.unifiedpush.api.Variant;
import org.jboss.aerogear.unifiedpush.message.UnifiedPushMessage;
import org.jboss.aerogear.unifiedpush.service.ClientInstallationService;
import javax.inject.Inject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
@SenderType(AndroidVariant.class)
public class GCMPushNotificationSender implements PushNotificationSender {
private static final int GCM_PAGE = 1000;
@Inject
private ClientInstallationService clientInstallationService;
private final Logger logger = Logger.getLogger(GCMPushNotificationSender.class.getName());
/**
* Sends GCM notifications ({@link UnifiedPushMessage}) to all devices, that are represented by
* the {@link List} of tokens for the given {@link AndroidVariant}.
*/
public void sendPushMessage(Variant variant, Collection<String> tokens, UnifiedPushMessage pushMessage, NotificationSenderCallback callback) {
// no need to send empty list
if (tokens.isEmpty()) {
return;
}
final List<String> registrationIDs = new ArrayList(tokens);
final AndroidVariant androidVariant = (AndroidVariant) variant;
// payload builder:
Builder gcmBuilder = new Message.Builder();
// add the "recognized" keys...
gcmBuilder.addData("alert", pushMessage.getAlert());
gcmBuilder.addData("sound", pushMessage.getSound());
gcmBuilder.addData("badge", "" + pushMessage.getBadge());
// if present, apply the time-to-live metadata:
int ttl = pushMessage.getTimeToLive();
if (ttl != -1) {
gcmBuilder.timeToLive(ttl);
}
// iterate over the missing keys:
Set<String> keys = pushMessage.getData().keySet();
for (String key : keys) {
// GCM needs stringified values:
gcmBuilder.addData(key, "" + pushMessage.getData().get(key));
}
Message gcmMessage = gcmBuilder.build();
// send it out.....
try {
logger.log(Level.FINE, "Sending transformed GCM payload: " + gcmMessage);
final Sender sender = new Sender(androidVariant.getGoogleKey());
// GCM does only allow a 1000 device IDs
while (! registrationIDs.isEmpty()) {
int toIndex = GCM_PAGE;
if (registrationIDs.size() < GCM_PAGE) {
toIndex = registrationIDs.size();
}
List<String> sublist = registrationIDs.subList(0, toIndex);
// send out a message to a few devices...
processGCM(androidVariant, sublist, gcmMessage, sender);
registrationIDs.removeAll(sublist);
}
logger.log(Level.INFO, "Message to GCM has been submitted");
callback.onSuccess();
} catch (Exception e) {
// GCM exceptions:
logger.log(Level.SEVERE, "Error sending payload to GCM server", e);
callback.onError("Error sending payload to GCM server");
}
}
/**
* Process the HTTP POST to the GCM infrastructor for the given list of registrationIDs. *
*/
private void processGCM(AndroidVariant androidVariant, List<String> registrationIDs, Message gcmMessage, Sender sender) throws IOException {
logger.log(Level.INFO, "Sending payload for [" + registrationIDs.size() + "] devices to GCM");
MulticastResult multicastResult = sender.send(gcmMessage, registrationIDs, 0);
// after sending, let's identify the inactive/invalid registrationIDs and trigger their deletion:
cleanupInvalidRegistrationIDsForVariant(androidVariant.getVariantID(), multicastResult, registrationIDs);
}
/**
* <p>Walks over the {@code MulticastResult} from the GCM call and identifies the <code>index</code> of all {@code Result} objects that
* indicate an <code>InvalidRegistration</code> error.
*
* <p>This <code>index</code> is used to find the matching <code>registration ID</code> in the List of all used <code>registrationIDs</code>.
*
* <p>Afterwards all 'invalid' registration IDs for the given <code>variantID</code> are being deleted from our database.
*
* @param variantID id of the actual {@code AndroidVariantEntity}.
* @param multicastResult the results from the HTTP request to the Google Cloud.
* @param registrationIDs list of all tokens that we submitted to GCM.
*/
private void cleanupInvalidRegistrationIDsForVariant(String variantID, MulticastResult multicastResult, List<String> registrationIDs) {
// get the GCM send results for all of the client devices:
final List<Result> results = multicastResult.getResults();
// storage for all the invalid registration IDs:
final Set<String> inactiveTokens = new HashSet<String>();
// read the results:
for (int i = 0; i < results.size(); i++) {
// use the current index to access the individual results
Result result = results.get(i);
// is there an error code that indicates an invalid regID ?
if (Constants.ERROR_INVALID_REGISTRATION.equals(result.getErrorCodeName())) {
// Ok the result at INDEX 'i' was an 'InvalidRegistration'!
// Now use the INDEX of the 'InvalidRegistration' result object, and look
// for the matching registrationID inside of the List that contains
// all the used registration IDs and store it:
inactiveTokens.add(registrationIDs.get(i));
}
}
// trigger asynchronous deletion:
logger.log(Level.FINE, "Deleting '" + inactiveTokens.size() + "' invalid Android installations");
clientInstallationService.removeInstallationsForVariantByDeviceTokens(variantID, inactiveTokens);
}
}