/* * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.whispersystems.textsecuregcm.push; import com.codahale.metrics.Gauge; import com.codahale.metrics.SharedMetricRegistries; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.push.ApnFallbackManager.ApnFallbackTask; import org.whispersystems.textsecuregcm.push.WebsocketSender.DeliveryStatus; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.storage.Device; import org.whispersystems.textsecuregcm.util.BlockingThreadPoolExecutor; import org.whispersystems.textsecuregcm.util.Constants; import org.whispersystems.textsecuregcm.util.Util; import org.whispersystems.textsecuregcm.websocket.WebsocketAddress; import java.util.concurrent.TimeUnit; import static com.codahale.metrics.MetricRegistry.name; import io.dropwizard.lifecycle.Managed; import static org.whispersystems.textsecuregcm.entities.MessageProtos.Envelope; public class PushSender implements Managed { private final Logger logger = LoggerFactory.getLogger(PushSender.class); public static final String APN_PAYLOAD = "{\"aps\":{\"sound\":\"default\",\"badge\":%d,\"alert\":{\"loc-key\":\"APN_Message\"}}}"; private final ApnFallbackManager apnFallbackManager; private final GCMSender gcmSender; private final APNSender apnSender; private final WebsocketSender webSocketSender; private final BlockingThreadPoolExecutor executor; private final int queueSize; public PushSender(ApnFallbackManager apnFallbackManager, GCMSender gcmSender, APNSender apnSender, WebsocketSender websocketSender, int queueSize) { this.apnFallbackManager = apnFallbackManager; this.gcmSender = gcmSender; this.apnSender = apnSender; this.webSocketSender = websocketSender; this.queueSize = queueSize; this.executor = new BlockingThreadPoolExecutor(50, queueSize); SharedMetricRegistries.getOrCreate(Constants.METRICS_NAME) .register(name(PushSender.class, "send_queue_depth"), new Gauge<Integer>() { @Override public Integer getValue() { return executor.getSize(); } }); } public void sendMessage(final Account account, final Device device, final Envelope message, final boolean silent) throws NotPushRegisteredException { if (device.getGcmId() == null && device.getApnId() == null && !device.getFetchesMessages()) { throw new NotPushRegisteredException("No delivery possible!"); } if (queueSize > 0) { executor.execute(new Runnable() { @Override public void run() { sendSynchronousMessage(account, device, message, silent); } }); } else { sendSynchronousMessage(account, device, message, silent); } } public void sendQueuedNotification(Account account, Device device, int messageQueueDepth, boolean fallback) throws NotPushRegisteredException, TransientPushFailureException { if (device.getGcmId() != null) sendGcmNotification(account, device); else if (device.getApnId() != null) sendApnNotification(account, device, messageQueueDepth, fallback); else if (!device.getFetchesMessages()) throw new NotPushRegisteredException("No notification possible!"); } public WebsocketSender getWebSocketSender() { return webSocketSender; } private void sendSynchronousMessage(Account account, Device device, Envelope message, boolean silent) { if (device.getGcmId() != null) sendGcmMessage(account, device, message); else if (device.getApnId() != null) sendApnMessage(account, device, message, silent); else if (device.getFetchesMessages()) sendWebSocketMessage(account, device, message); else throw new AssertionError(); } private void sendGcmMessage(Account account, Device device, Envelope message) { DeliveryStatus deliveryStatus = webSocketSender.sendMessage(account, device, message, WebsocketSender.Type.GCM); if (!deliveryStatus.isDelivered()) { sendGcmNotification(account, device); } } private void sendGcmNotification(Account account, Device device) { GcmMessage gcmMessage = new GcmMessage(device.getGcmId(), account.getNumber(), (int)device.getId(), false); gcmSender.sendMessage(gcmMessage); } private void sendApnMessage(Account account, Device device, Envelope outgoingMessage, boolean silent) { DeliveryStatus deliveryStatus = webSocketSender.sendMessage(account, device, outgoingMessage, WebsocketSender.Type.APN); if (!deliveryStatus.isDelivered() && outgoingMessage.getType() != Envelope.Type.RECEIPT) { boolean fallback = !silent && !outgoingMessage.getSource().equals(account.getNumber()); sendApnNotification(account, device, deliveryStatus.getMessageQueueDepth(), fallback); } } private void sendApnNotification(Account account, Device device, int messageQueueDepth, boolean fallback) { ApnMessage apnMessage; if (!Util.isEmpty(device.getVoipApnId())) { apnMessage = new ApnMessage(device.getVoipApnId(), account.getNumber(), (int)device.getId(), String.format(APN_PAYLOAD, messageQueueDepth), true, System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(ApnFallbackManager.FALLBACK_DURATION)); if (fallback) { apnFallbackManager.schedule(new WebsocketAddress(account.getNumber(), device.getId()), new ApnFallbackTask(device.getApnId(), device.getVoipApnId(), apnMessage)); } } else { apnMessage = new ApnMessage(device.getApnId(), account.getNumber(), (int)device.getId(), String.format(APN_PAYLOAD, messageQueueDepth), false, ApnMessage.MAX_EXPIRATION); } try { apnSender.sendMessage(apnMessage); } catch (TransientPushFailureException e) { logger.warn("SILENT PUSH LOSS", e); } } private void sendWebSocketMessage(Account account, Device device, Envelope outgoingMessage) { webSocketSender.sendMessage(account, device, outgoingMessage, WebsocketSender.Type.WEB); } @Override public void start() throws Exception { apnSender.start(); gcmSender.start(); } @Override public void stop() throws Exception { executor.shutdown(); executor.awaitTermination(5, TimeUnit.MINUTES); apnSender.stop(); gcmSender.stop(); } }