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