001/** 002 * 003 * Copyright 2003-2006 Jive Software, 2014 Florian Schmaus 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018package org.jivesoftware.smackx.iqlast; 019 020import java.util.Map; 021import java.util.WeakHashMap; 022 023import org.jivesoftware.smack.ConnectionCreationListener; 024import org.jivesoftware.smack.Manager; 025import org.jivesoftware.smack.SmackException.NoResponseException; 026import org.jivesoftware.smack.SmackException.NotConnectedException; 027import org.jivesoftware.smack.StanzaListener; 028import org.jivesoftware.smack.XMPPConnection; 029import org.jivesoftware.smack.XMPPConnectionRegistry; 030import org.jivesoftware.smack.XMPPException.XMPPErrorException; 031import org.jivesoftware.smack.filter.StanzaTypeFilter; 032import org.jivesoftware.smack.iqrequest.AbstractIqRequestHandler; 033import org.jivesoftware.smack.iqrequest.IQRequestHandler.Mode; 034import org.jivesoftware.smack.packet.IQ; 035import org.jivesoftware.smack.packet.IQ.Type; 036import org.jivesoftware.smack.packet.Message; 037import org.jivesoftware.smack.packet.Presence; 038import org.jivesoftware.smack.packet.Stanza; 039import org.jivesoftware.smack.packet.XMPPError.Condition; 040 041import org.jivesoftware.smackx.disco.ServiceDiscoveryManager; 042import org.jivesoftware.smackx.iqlast.packet.LastActivity; 043 044import org.jxmpp.jid.Jid; 045 046/** 047 * A last activity manager for handling information about the last activity 048 * associated with a Jabber ID. A manager handles incoming LastActivity requests 049 * of existing Connections. It also allows to request last activity information 050 * of other users. 051 * <p> 052 * 053 * LastActivity (XEP-0012) based on the sending JID's type allows for retrieval 054 * of: 055 * <ol> 056 * <li>How long a particular user has been idle 057 * <li>How long a particular user has been logged-out and the message the 058 * specified when doing so. 059 * <li>How long a host has been up. 060 * </ol> 061 * <p/> 062 * 063 * For example to get the idle time of a user logged in a resource, simple send 064 * the LastActivity stanza(/packet) to them, as in the following code: 065 * <p> 066 * 067 * <pre> 068 * XMPPConnection con = new XMPPTCPConnection("jabber.org"); 069 * con.login("john", "doe"); 070 * LastActivity activity = LastActivity.getLastActivity(con, "xray@jabber.org/Smack"); 071 * </pre> 072 * 073 * To get the lapsed time since the last user logout is the same as above but 074 * with out the resource: 075 * 076 * <pre> 077 * LastActivity activity = LastActivity.getLastActivity(con, "xray@jabber.org"); 078 * </pre> 079 * 080 * To get the uptime of a host, you simple send the LastActivity stanza(/packet) to it, 081 * as in the following code example: 082 * <p> 083 * 084 * <pre> 085 * LastActivity activity = LastActivity.getLastActivity(con, "jabber.org"); 086 * </pre> 087 * 088 * @author Gabriel Guardincerri 089 * @author Florian Schmaus 090 * @see <a href="http://xmpp.org/extensions/xep-0012.html">XEP-0012: Last 091 * Activity</a> 092 */ 093 094public final class LastActivityManager extends Manager { 095 private static final Map<XMPPConnection, LastActivityManager> instances = new WeakHashMap<XMPPConnection, LastActivityManager>(); 096// private static final PacketFilter IQ_GET_LAST_FILTER = new AndFilter(IQTypeFilter.GET, 097// new StanzaTypeFilter(LastActivity.class)); 098 099 private static boolean enabledPerDefault = true; 100 101 /** 102 * Enable or disable Last Activity for new XMPPConnections. 103 * 104 * @param enabledPerDefault 105 */ 106 public static void setEnabledPerDefault(boolean enabledPerDefault) { 107 LastActivityManager.enabledPerDefault = enabledPerDefault; 108 } 109 110 // Enable the LastActivity support on every established connection 111 static { 112 XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() { 113 @Override 114 public void connectionCreated(XMPPConnection connection) { 115 LastActivityManager.getInstanceFor(connection); 116 } 117 }); 118 } 119 120 public static synchronized LastActivityManager getInstanceFor(XMPPConnection connection) { 121 LastActivityManager lastActivityManager = instances.get(connection); 122 if (lastActivityManager == null) 123 lastActivityManager = new LastActivityManager(connection); 124 return lastActivityManager; 125 } 126 127 private volatile long lastMessageSent; 128 private boolean enabled = false; 129 130 /** 131 * Creates a last activity manager to response last activity requests. 132 * 133 * @param connection 134 * The XMPPConnection that the last activity requests will use. 135 */ 136 private LastActivityManager(XMPPConnection connection) { 137 super(connection); 138 139 // Listen to all the sent messages to reset the idle time on each one 140 connection.addPacketSendingListener(new StanzaListener() { 141 @Override 142 public void processStanza(Stanza packet) { 143 Presence presence = (Presence) packet; 144 Presence.Mode mode = presence.getMode(); 145 if (mode == null) return; 146 switch (mode) { 147 case available: 148 case chat: 149 // We assume that only a switch to available and chat indicates user activity 150 // since other mode changes could be also a result of some sort of automatism 151 resetIdleTime(); 152 break; 153 default: 154 break; 155 } 156 } 157 }, StanzaTypeFilter.PRESENCE); 158 159 connection.addPacketSendingListener(new StanzaListener() { 160 @Override 161 public void processStanza(Stanza packet) { 162 Message message = (Message) packet; 163 // if it's not an error message, reset the idle time 164 if (message.getType() == Message.Type.error) return; 165 resetIdleTime(); 166 } 167 }, StanzaTypeFilter.MESSAGE); 168 169 // Register a listener for a last activity query 170 connection.registerIQRequestHandler(new AbstractIqRequestHandler(LastActivity.ELEMENT, LastActivity.NAMESPACE, 171 Type.get, Mode.async) { 172 @Override 173 public IQ handleIQRequest(IQ iqRequest) { 174 if (!enabled) 175 return IQ.createErrorResponse(iqRequest, Condition.not_acceptable); 176 LastActivity message = new LastActivity(); 177 message.setType(IQ.Type.result); 178 message.setTo(iqRequest.getFrom()); 179 message.setFrom(iqRequest.getTo()); 180 message.setStanzaId(iqRequest.getStanzaId()); 181 message.setLastActivity(getIdleTime()); 182 183 return message; 184 } 185 }); 186 187 if (enabledPerDefault) { 188 enable(); 189 } 190 resetIdleTime(); 191 instances.put(connection, this); 192 } 193 194 public synchronized void enable() { 195 ServiceDiscoveryManager.getInstanceFor(connection()).addFeature(LastActivity.NAMESPACE); 196 enabled = true; 197 } 198 199 public synchronized void disable() { 200 ServiceDiscoveryManager.getInstanceFor(connection()).removeFeature(LastActivity.NAMESPACE); 201 enabled = false; 202 } 203 204 /** 205 * Resets the idle time to 0, this should be invoked when a new message is 206 * sent. 207 */ 208 private void resetIdleTime() { 209 lastMessageSent = System.currentTimeMillis(); 210 } 211 212 /** 213 * The idle time is the lapsed time between the last message sent and now. 214 * 215 * @return the lapsed time between the last message sent and now. 216 */ 217 private long getIdleTime() { 218 long lms = lastMessageSent; 219 long now = System.currentTimeMillis(); 220 return ((now - lms) / 1000); 221 } 222 223 /** 224 * Returns the last activity of a particular jid. If the jid is a full JID 225 * (i.e., a JID of the form of 'user@host/resource') then the last activity 226 * is the idle time of that connected resource. On the other hand, when the 227 * jid is a bare JID (e.g. 'user@host') then the last activity is the lapsed 228 * time since the last logout or 0 if the user is currently logged in. 229 * Moreover, when the jid is a server or component (e.g., a JID of the form 230 * 'host') the last activity is the uptime. 231 * 232 * @param jid 233 * the JID of the user. 234 * @return the LastActivity stanza(/packet) of the jid. 235 * @throws XMPPErrorException 236 * thrown if a server error has occured. 237 * @throws NoResponseException if there was no response from the server. 238 * @throws NotConnectedException 239 * @throws InterruptedException 240 */ 241 public LastActivity getLastActivity(Jid jid) throws NoResponseException, XMPPErrorException, 242 NotConnectedException, InterruptedException { 243 LastActivity activity = new LastActivity(jid); 244 return (LastActivity) connection().createStanzaCollectorAndSend(activity).nextResultOrThrow(); 245 } 246 247 /** 248 * Returns true if Last Activity (XEP-0012) is supported by a given JID. 249 * 250 * @param jid a JID to be tested for Last Activity support 251 * @return true if Last Activity is supported, otherwise false 252 * @throws NotConnectedException 253 * @throws XMPPErrorException 254 * @throws NoResponseException 255 * @throws InterruptedException 256 */ 257 public boolean isLastActivitySupported(Jid jid) throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException { 258 return ServiceDiscoveryManager.getInstanceFor(connection()).supportsFeature(jid, LastActivity.NAMESPACE); 259 } 260}