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