| /** |
| * Copyright 2012-2013 Florian Schmaus |
| * |
| * All rights reserved. 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.jivesoftware.smack.keepalive; |
| |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.concurrent.ScheduledExecutorService; |
| import java.util.concurrent.ScheduledFuture; |
| import java.util.concurrent.ScheduledThreadPoolExecutor; |
| import java.util.concurrent.ThreadFactory; |
| import java.util.concurrent.TimeUnit; |
| |
| import org.jivesoftware.smack.Connection; |
| import org.jivesoftware.smack.ConnectionCreationListener; |
| import org.jivesoftware.smack.ConnectionListener; |
| import org.jivesoftware.smack.PacketCollector; |
| import org.jivesoftware.smack.PacketListener; |
| import org.jivesoftware.smack.SmackConfiguration; |
| import org.jivesoftware.smack.filter.PacketFilter; |
| import org.jivesoftware.smack.filter.PacketIDFilter; |
| import org.jivesoftware.smack.packet.Packet; |
| import org.jivesoftware.smack.ping.PingFailedListener; |
| import org.jivesoftware.smack.ping.packet.Ping; |
| |
| /** |
| * Using an implementation of <a href="http://www.xmpp.org/extensions/xep-0199.html">XMPP Ping (XEP-0199)</a>. This |
| * class provides keepalive functionality with the server that will periodically "ping" the server to maintain and/or |
| * verify that the connection still exists. |
| * <p> |
| * The ping is done at the application level and is therefore protocol agnostic. It will thus work for both standard TCP |
| * connections as well as BOSH or any other transport protocol. It will also work regardless of whether the server |
| * supports the Ping extension, since an error response to the ping serves the same purpose as a pong. |
| * |
| * @author Florian Schmaus |
| */ |
| public class KeepAliveManager { |
| private static Map<Connection, KeepAliveManager> instances = new HashMap<Connection, KeepAliveManager>(); |
| private static volatile ScheduledExecutorService periodicPingExecutorService; |
| |
| static { |
| if (SmackConfiguration.getKeepAliveInterval() > 0) { |
| Connection.addConnectionCreationListener(new ConnectionCreationListener() { |
| public void connectionCreated(Connection connection) { |
| new KeepAliveManager(connection); |
| } |
| }); |
| } |
| } |
| |
| private Connection connection; |
| private long pingInterval = SmackConfiguration.getKeepAliveInterval(); |
| private Set<PingFailedListener> pingFailedListeners = Collections.synchronizedSet(new HashSet<PingFailedListener>()); |
| private volatile ScheduledFuture<?> periodicPingTask; |
| private volatile long lastSuccessfulContact = -1; |
| |
| /** |
| * Retrieves a {@link KeepAliveManager} for the specified {@link Connection}, creating one if it doesn't already |
| * exist. |
| * |
| * @param connection |
| * The connection the manager is attached to. |
| * @return The new or existing manager. |
| */ |
| public synchronized static KeepAliveManager getInstanceFor(Connection connection) { |
| KeepAliveManager pingManager = instances.get(connection); |
| |
| if (pingManager == null) { |
| pingManager = new KeepAliveManager(connection); |
| instances.put(connection, pingManager); |
| } |
| return pingManager; |
| } |
| |
| /* |
| * Start the executor service if it hasn't been started yet. |
| */ |
| private synchronized static void enableExecutorService() { |
| if (periodicPingExecutorService == null) { |
| periodicPingExecutorService = new ScheduledThreadPoolExecutor(1, new ThreadFactory() { |
| @Override |
| public Thread newThread(Runnable runnable) { |
| Thread pingThread = new Thread(runnable, "Smack Keepalive"); |
| pingThread.setDaemon(true); |
| return pingThread; |
| } |
| }); |
| } |
| } |
| |
| /* |
| * Stop the executor service if all monitored connections are disconnected. |
| */ |
| private synchronized static void handleDisconnect(Connection con) { |
| if (periodicPingExecutorService != null) { |
| instances.remove(con); |
| |
| if (instances.isEmpty()) { |
| periodicPingExecutorService.shutdownNow(); |
| periodicPingExecutorService = null; |
| } |
| } |
| } |
| |
| private KeepAliveManager(Connection connection) { |
| this.connection = connection; |
| init(); |
| handleConnect(); |
| } |
| |
| /* |
| * Call after every connection to add the packet listener. |
| */ |
| private void handleConnect() { |
| // Listen for all incoming packets and reset the scheduled ping whenever |
| // one arrives. |
| connection.addPacketListener(new PacketListener() { |
| |
| @Override |
| public void processPacket(Packet packet) { |
| // reschedule the ping based on this last server contact |
| lastSuccessfulContact = System.currentTimeMillis(); |
| schedulePingServerTask(); |
| } |
| }, null); |
| } |
| |
| private void init() { |
| connection.addConnectionListener(new ConnectionListener() { |
| |
| @Override |
| public void connectionClosed() { |
| stopPingServerTask(); |
| handleDisconnect(connection); |
| } |
| |
| @Override |
| public void connectionClosedOnError(Exception arg0) { |
| stopPingServerTask(); |
| handleDisconnect(connection); |
| } |
| |
| @Override |
| public void reconnectionSuccessful() { |
| handleConnect(); |
| schedulePingServerTask(); |
| } |
| |
| @Override |
| public void reconnectingIn(int seconds) { |
| } |
| |
| @Override |
| public void reconnectionFailed(Exception e) { |
| } |
| }); |
| |
| instances.put(connection, this); |
| schedulePingServerTask(); |
| } |
| |
| /** |
| * Sets the ping interval. |
| * |
| * @param pingInterval |
| * The new ping time interval in milliseconds. |
| */ |
| public void setPingInterval(long newPingInterval) { |
| if (pingInterval == newPingInterval) |
| return; |
| |
| // Enable the executor service |
| if (newPingInterval > 0) |
| enableExecutorService(); |
| |
| pingInterval = newPingInterval; |
| |
| if (pingInterval < 0) { |
| stopPinging(); |
| } |
| else { |
| schedulePingServerTask(); |
| } |
| } |
| |
| /** |
| * Stops pinging the server. This cannot stop a ping that has already started, but will prevent another from being triggered. |
| * <p> |
| * To restart, call {@link #setPingInterval(long)}. |
| */ |
| public void stopPinging() { |
| pingInterval = -1; |
| stopPingServerTask(); |
| } |
| |
| /** |
| * Gets the ping interval. |
| * |
| * @return The ping interval in milliseconds. |
| */ |
| public long getPingInterval() { |
| return pingInterval; |
| } |
| |
| /** |
| * Add listener for notification when a server ping fails. |
| * |
| * <p> |
| * Please note that this doesn't necessarily mean that the connection is lost, a slow to respond server could also |
| * cause a failure due to taking too long to respond and thus causing a reply timeout. |
| * |
| * @param listener |
| * The listener to be called |
| */ |
| public void addPingFailedListener(PingFailedListener listener) { |
| pingFailedListeners.add(listener); |
| } |
| |
| /** |
| * Remove the listener. |
| * |
| * @param listener |
| * The listener to be removed. |
| */ |
| public void removePingFailedListener(PingFailedListener listener) { |
| pingFailedListeners.remove(listener); |
| } |
| |
| /** |
| * Returns the elapsed time (in milliseconds) since the last successful contact with the server |
| * (i.e. the last time any message was received). |
| * <p> |
| * <b>Note</b>: Result is -1 if no message has been received since manager was created and |
| * 0 if the elapsed time is negative due to a clock reset. |
| * |
| * @return Elapsed time since last message was received. |
| */ |
| public long getTimeSinceLastContact() { |
| if (lastSuccessfulContact == -1) |
| return lastSuccessfulContact; |
| long delta = System.currentTimeMillis() - lastSuccessfulContact; |
| |
| return (delta < 0) ? 0 : delta; |
| } |
| |
| /** |
| * Cancels any existing periodic ping task if there is one and schedules a new ping task if pingInterval is greater |
| * then zero. |
| * |
| * This is designed so only one executor is used for scheduling all pings on all connections. This results in only 1 thread used for pinging. |
| */ |
| private synchronized void schedulePingServerTask() { |
| enableExecutorService(); |
| stopPingServerTask(); |
| |
| if (pingInterval > 0) { |
| periodicPingTask = periodicPingExecutorService.schedule(new Runnable() { |
| @Override |
| public void run() { |
| Ping ping = new Ping(); |
| PacketFilter responseFilter = new PacketIDFilter(ping.getPacketID()); |
| final PacketCollector response = pingFailedListeners.isEmpty() ? null : connection.createPacketCollector(responseFilter); |
| connection.sendPacket(ping); |
| |
| if (response != null) { |
| // Schedule a collector for the ping reply, notify listeners if none is received. |
| periodicPingExecutorService.schedule(new Runnable() { |
| @Override |
| public void run() { |
| Packet result = response.nextResult(1); |
| |
| // Stop queuing results |
| response.cancel(); |
| |
| // The actual result of the reply can be ignored since we only care if we actually got one. |
| if (result == null) { |
| for (PingFailedListener listener : pingFailedListeners) { |
| listener.pingFailed(); |
| } |
| } |
| } |
| }, SmackConfiguration.getPacketReplyTimeout(), TimeUnit.MILLISECONDS); |
| } |
| } |
| }, getPingInterval(), TimeUnit.MILLISECONDS); |
| } |
| } |
| |
| private void stopPingServerTask() { |
| if (periodicPingTask != null) { |
| periodicPingTask.cancel(true); |
| periodicPingTask = null; |
| } |
| } |
| } |