/** @author Kir */ public class JabberTransport implements Transport, ConnectionListener, Disposable { @NonNls private static final Logger LOG = Logger.getLogger(JabberTransport.class); private static final int RESPONSE_TIMEOUT = 120 * 1000; @NonNls public static final String CODE = "Jabber"; private final JabberUI myUI; private final JabberFacade myFacade; private final UserModel myUserModel; private final IDEtalkListener myUserModelListener = new MyUserModelListener(); private final AsyncMessageDispatcher myDispatcher; private RosterListener myRosterListener; private PacketListener mySubscribeListener; private PacketListener myMessageListener; private final JabberUserFinder myUserFinder; private final IDEFacade myIdeFacade; private final String myThreadIdPrefix = StringUtils.randomString(5); private int myCurrentThreadId; private boolean myIgnoreUserEvents; private PresenceMode myPresenceMode; private final Map<User, UserPresence> myUser2Presence = new HashMap<>(); private final Set<String> myIDEtalkUsers = new HashSet<>(); private final Map<String, String> myUser2Thread = Collections.synchronizedMap(new HashMap<String, String>()); @NonNls private static final String RESPONSE = "response"; private final IgnoreList myIgnoreList; // negative value disables reconnect private int myReconnectTimeout = Integer.parseInt(System.getProperty("ideTalk.reconnect", "30")) * 1000; private Future<?> myReconnectProcess; public JabberTransport( JabberUI UI, JabberFacade facade, UserModel userModel, AsyncMessageDispatcher messageDispatcher, JabberUserFinder userFinder) { Roster.setDefaultSubscriptionMode(Roster.SubscriptionMode.manual); // XMPPConnection.DEBUG_ENABLED = true; JDOMExtension.init(); myUI = UI; myFacade = facade; myUserModel = userModel; myDispatcher = messageDispatcher; myUserFinder = userFinder; myIdeFacade = messageDispatcher.getIdeFacade(); myIgnoreList = new IgnoreList(myIdeFacade); myFacade.addConnectionListener(this); getBroadcaster().addListener(myUserModelListener); } private EventBroadcaster getBroadcaster() { return myUserModel.getBroadcaster(); } @Override public String getName() { return CODE; } @Override public void initializeProject(String projectName, MutablePicoContainer projectLevelContainer) { myUI.initPerProject(projectLevelContainer); myIdeFacade.runOnPooledThread(() -> myFacade.connect()); } @Override public User[] findUsers(ProgressIndicator progressIndicator) { if (isOnline()) { return myUserFinder.findUsers(progressIndicator); } return new User[0]; } @Override public Class<? extends NamedUserCommand> getSpecificFinderClass() { return FindByJabberIdCommand.class; } @Override public boolean isOnline() { return myFacade.isConnectedAndAuthenticated(); } @Override public UserPresence getUserPresence(User user) { UserPresence presence = myUser2Presence.get(user); if (presence == null) { presence = new UserPresence(false); myUser2Presence.put(user, presence); } return presence; } private UserPresence _getUserPresence(User user) { Presence presence = _getPresence(user); if (presence != null && presence.getType() == Presence.Type.available) { Presence.Mode mode = presence.getMode(); final PresenceMode presenceMode; //noinspection IfStatementWithTooManyBranches if (mode == Presence.Mode.away) { presenceMode = PresenceMode.AWAY; } else if (mode == Presence.Mode.dnd) { presenceMode = PresenceMode.DND; } else if (mode == Presence.Mode.xa) { presenceMode = PresenceMode.EXTENDED_AWAY; } else { presenceMode = PresenceMode.AVAILABLE; } return new UserPresence(presenceMode); } return new UserPresence(false); } @Override @NonNls public Icon getIcon(UserPresence userPresence) { return UIUtil.getIcon( userPresence, IdetalkCoreIcons.IdeTalk.Jabber, IdetalkCoreIcons.IdeTalk.Jabber_dnd); } @Override public boolean isSelf(User user) { return myFacade.isConnectedAndAuthenticated() && getSimpleId(myFacade.getConnection().getUser()).equals(user.getName()); } @Override public String[] getProjects(User user) { return ArrayUtil.EMPTY_STRING_ARRAY; } @Override @Nullable public String getAddressString(User user) { return null; } @Override public synchronized void sendXmlMessage(User user, final XmlMessage xmlMessage) { if (!myUI.connectAndLogin(null)) { return; } final String threadId = getThreadId(user); final PacketCollector packetCollector = myFacade.getConnection().createPacketCollector(new ThreadFilter(threadId)); doSendMessage(xmlMessage, user, threadId); if (xmlMessage.needsResponse()) { //noinspection HardCodedStringLiteral final Runnable responseWaiterRunnable = () -> { try { processResponse(xmlMessage, packetCollector); } finally { packetCollector.cancel(); } }; myIdeFacade.runOnPooledThread(responseWaiterRunnable); } else { packetCollector.cancel(); } } String getThreadId(User user) { String id = myUser2Thread.get(user.getName()); if (id == null) { id = myThreadIdPrefix + myCurrentThreadId++; myUser2Thread.put(user.getName(), id); } return id; } @Override public void setOwnPresence(UserPresence userPresence) { if (isOnline() && !userPresence.isOnline()) { myFacade.disconnect(); } else if (!isOnline() && userPresence.isOnline()) { myUI.connectAndLogin(null); } if (isOnline() && presenceModeChanged(userPresence.getPresenceMode())) { myFacade.setOnlinePresence(userPresence); myPresenceMode = userPresence.getPresenceMode(); } } @Override public boolean hasIdeTalkClient(User user) { return myIDEtalkUsers.contains(user.getName()); } private boolean presenceModeChanged(PresenceMode presenceMode) { return myPresenceMode == null || myPresenceMode != presenceMode; } private static void processResponse(XmlMessage xmlMessage, PacketCollector collector) { boolean gotResponse = false; while (!gotResponse) { Message response = (Message) collector.nextResult(RESPONSE_TIMEOUT); if (response == null) break; final Collection<PacketExtension> extensions = response.getExtensions(); for (PacketExtension o : extensions) { if (o instanceof JDOMExtension) { JDOMExtension extension = (JDOMExtension) o; if (RESPONSE.equals(extension.getElement().getName())) { xmlMessage.processResponse(extension.getElement()); gotResponse = true; break; } } } } } private Message doSendMessage(XmlMessage xmlMessage, User user, String threadId) { Element element = new Element(xmlMessage.getTagName(), xmlMessage.getTagNamespace()); xmlMessage.fillRequest(element); Message message = createBaseMessage(user, element.getText()); message.setThread(threadId); message.addExtension(new JDOMExtension(element)); myFacade.getConnection().sendPacket(message); return message; } static Message createBaseMessage(User user, String message) { Message msg = new Message(user.getName(), Message.Type.CHAT); msg.setBody(message); return msg; } @Override public void connected(XMPPConnection connection) { LOG.info("Jabber connected"); if (mySubscribeListener == null) { mySubscribeListener = new MySubscribeListener(); connection.addPacketListener(mySubscribeListener, new PacketTypeFilter(Presence.class)); } if (myMessageListener == null) { myMessageListener = new MyMessageListener(); connection.addPacketListener(myMessageListener, new PacketTypeFilter(Message.class)); } } @Override public void authenticated() { LOG.info("Jabber authenticated: " + myFacade.getConnection().getUser()); if (myRosterListener == null) { myRosterListener = new MyRosterListener(); getRoster().addRosterListener(myRosterListener); } myUserFinder.registerForProject(myFacade.getMyAccount().getJabberId()); if (!hasJabberContacts()) { synchronizeRoster(false); } } private boolean hasJabberContacts() { User[] users = myUserModel.getAllUsers(); for (User user : users) { if (user.getTransportCode().equals(getName())) return true; } return false; } @Override public void disconnected(boolean onError) { final XMPPConnection connection = myFacade.getConnection(); LOG.info("Jabber disconnected: " + connection.getUser()); connection.removePacketListener(mySubscribeListener); mySubscribeListener = null; connection.removePacketListener(myMessageListener); myMessageListener = null; final Roster roster = connection.getRoster(); if (roster != null) { roster.removeRosterListener(myRosterListener); } myRosterListener = null; myIDEtalkUsers.clear(); myUser2Presence.clear(); myUser2Thread.clear(); if (onError && reconnectEnabledAndNotStarted()) { LOG.warn(getMsg("jabber.server.was.disconnected", myReconnectTimeout / 1000)); myReconnectProcess = myIdeFacade.runOnPooledThread(new MyReconnectRunnable()); } } private boolean reconnectEnabledAndNotStarted() { return (myReconnectProcess == null || myReconnectProcess.isDone()) && myReconnectTimeout >= 0; } @Override public void dispose() { getBroadcaster().removeListener(myUserModelListener); myFacade.removeConnectionListener(this); } private void updateUserPresence(String jabberId) { LOG.debug("Presence changed for " + jabberId); final User user = myUserModel.findUser(getSimpleId(jabberId), getName()); if (user != null) { updateIsIDEtalkClient(jabberId, user); final UserPresence presence = _getUserPresence(user); IDEtalkEvent event = createPresenceChangeEvent(user, presence); if (event != null) { getBroadcaster().doChange(event, () -> myUser2Presence.put(user, presence)); } } } private void updateIsIDEtalkClient(String jabberId, User user) { if (getResource(jabberId) .toLowerCase() .startsWith(JabberFacade.IDETALK_RESOURCE.toLowerCase())) { myIDEtalkUsers.add(user.getName()); } else { myIDEtalkUsers.remove(user.getName()); } } @Nullable private IDEtalkEvent createPresenceChangeEvent(User user, UserPresence newPresence) { UserPresence oldPresence = getUserPresence(user); if (!newPresence.equals(oldPresence)) { if (newPresence.isOnline() ^ oldPresence.isOnline()) { return newPresence.isOnline() ? new UserEvent.Online(user) : new UserEvent.Offline(user); } else { return new UserEvent.Updated( user, PRESENCE, oldPresence.getPresenceMode(), newPresence.getPresenceMode()); } } return null; } private void updateJabberUsers(boolean removeUsersNotInRoster) { LOG.debug("Roster changed - update user model"); Set<User> currentUsers = new HashSet<>(Arrays.asList(myUserModel.getAllUsers())); for (RosterEntry rosterEntry : getRoster().getEntries()) { User user = addJabberUserToUserModelOrUpdateInfo(rosterEntry); currentUsers.remove(user); } if (removeUsersNotInRoster) { removeUsers(currentUsers); } if (LOG.isDebugEnabled()) { LOG.debug("Roster synchronized: " + Arrays.asList(myUserModel.getAllUsers())); } } private void removeUsers(Set<User> currentUsers) { for (User user : currentUsers) { myUserModel.removeUser(user); } } private User addJabberUserToUserModelOrUpdateInfo(RosterEntry rosterEntry) { // System.out.println("rosterEntry.getName() = " + rosterEntry.getName()); // System.out.println("rosterEntry.getUser() = " + rosterEntry.getUser()); User user = myUserModel.createUser(getSimpleId(rosterEntry.getUser()), getName()); String newGroup = getUserGroup(rosterEntry); if (newGroup != null) { user.setGroup(newGroup, myUserModel); } user.setDisplayName(rosterEntry.getName(), myUserModel); myUserModel.addUser(user); String jabberId = getCurrentJabberID(user, rosterEntry); updateIsIDEtalkClient(jabberId, user); return user; } private String getCurrentJabberID(User user, RosterEntry rosterEntry) { Presence presence = _getPresence(user); String jabberId = null; if (presence != null) { jabberId = presence.getFrom(); } if (jabberId == null) jabberId = rosterEntry.getUser(); if (jabberId == null) jabberId = rosterEntry.getName(); return jabberId; } static String getResource(String userName) { int lastSlash = userName.indexOf('/'); if (lastSlash != -1) { return userName.substring(lastSlash + 1); } return ""; } static String getSimpleId(String userName) { String id = userName; int lastSlash = id.indexOf('/'); if (lastSlash != -1) { id = id.substring(0, lastSlash); } return id; } @Nullable private static String getUserGroup(RosterEntry rosterEntry) { String group = null; for (RosterGroup rosterGroup : rosterEntry.getGroups()) { group = rosterGroup.getName(); } return group; } private Roster getRoster() { final Roster roster = myFacade.getConnection().getRoster(); assert roster != null; return roster; } public JabberFacade getFacade() { return myFacade; } boolean isUserInMyContactListAndActive(String userName) { User user = myUserModel.findUser(getSimpleId(userName), getName()); return user != null && user.isOnline(); } @Nullable private Presence _getPresence(User user) { if (!isOnline()) return null; return getRoster().getPresence(user.getName()); } private User self() { return myUserModel.createUser(myFacade.getMyAccount().getJabberId(), getName()); } public static JabberTransport getInstance() { return (JabberTransport) Pico.getInstance().getComponentInstanceOfType(JabberTransport.class); } public void synchronizeRoster(boolean removeUsersNotInRoster) { updateJabberUsers(removeUsersNotInRoster); } public void runIngnoringUserEvents(Runnable runnable) { try { myIgnoreUserEvents = true; runnable.run(); } finally { myIgnoreUserEvents = false; } } /** -1 disables reconnect */ public void setReconnectTimeout(int milliseconds) { myReconnectTimeout = milliseconds; } private class MyRosterListener implements RosterListener { @Override public void entriesAdded(Collection addresses) { updateJabberUsers(false); } @Override public void entriesUpdated(Collection addresses) { updateJabberUsers(false); } @Override public void entriesDeleted(Collection addresses) { updateJabberUsers(false); } @Override public void presenceChanged(final String string) { updateUserPresence(string); } } @SuppressWarnings({"RefusedBequest"}) private class MyUserModelListener extends TransportUserListener { MyUserModelListener() { super(JabberTransport.this); } @Override protected void processBeforeChange(UserEvent event) { super.processBeforeChange(event); event.accept( new EventVisitor() { @Override public void visitUserAdded(UserEvent.Added event) { event.getUser().setCanAccessMyFiles(false, myUserModel); } }); } @Override protected void processAfterChange(UserEvent event) { if (myIgnoreUserEvents) return; event.accept( new EventVisitor() { @Override public void visitUserRemoved(UserEvent.Removed event) { synchronizeWithJabberIfPossible(event); } @Override public void visitUserUpdated(UserEvent.Updated event) { if (GROUP.equals(event.getPropertyName()) || DISPLAY_NAME.equals(event.getPropertyName())) { synchronizeWithJabberIfPossible(event); } } }); } private void synchronizeWithJabberIfPossible(UserEvent event) { if (event.getUser().getTransportCode().equals(getName()) && myFacade.isConnectedAndAuthenticated()) { myDispatcher.sendNow(self(), new JabberSyncUserMessage(event)); } } } private class MySubscribeListener implements PacketListener { @Override public void processPacket(Packet packet) { final Presence presence = ((Presence) packet); if (presence.getType() != Presence.Type.subscribe) return; LOG.info("Subscribe request from " + presence.getFrom()); if (myIgnoreList.isIgnored(presence.getFrom())) { LOG.info(presence.getFrom() + " in ignore list"); return; } if (isUserInMyContactListAndActive(presence.getFrom()) || Pico.isUnitTest()) { acceptSubscription(presence, true); return; } UIUtil.invokeLater( () -> acceptSubscription(presence, myUI.shouldAcceptSubscriptionRequest(presence))); } private void acceptSubscription(final Presence presence, boolean subscribe) { if (!isOnline()) return; myFacade.changeSubscription(presence.getFrom(), subscribe); if (subscribe) { String from = getSimpleId(presence.getFrom()); LOG.info("Add " + from + " to the roster"); try { getRoster().createEntry(from, from, new String[] {UserModel.DEFAULT_GROUP}); } catch (XMPPException e) { LOG.warn(e); } } } } private class MyMessageListener implements PacketListener { @Override public void processPacket(Packet packet) { try { doProcessPacket(packet); } catch (Throwable e) { LOG.error(e.getMessage(), e); } } private void doProcessPacket(Packet packet) { final Message message = ((Message) packet); if (message.getType() == Message.Type.ERROR) { UIUtil.invokeLater( () -> { String from = (message.getFrom() != null) ? getMsg("from.0.lf", message.getFrom()) : ""; LOG.warn( getMsg( "jabber.error.text", from, (message.getError() == null ? "N/A" : message.getError().toString()))); }); return; } if (myIgnoreList.isIgnored(packet.getFrom())) { return; } Element element = null; for (PacketExtension o : message.getExtensions()) { if (o instanceof JDOMExtension) { element = ((JDOMExtension) o).getElement(); } } if (element != null && !RESPONSE.equals(element.getName())) { processAndSendResponse(element, message); } else if (element == null && message.getBody() != null) { // Some simple Jabber Message MessageEvent event = EventFactory.createMessageEvent( JabberTransport.this, getFrom(message), message.getBody()); if (message.getThread() != null) { myUser2Thread.put(getFrom(message), message.getThread()); } getBroadcaster().fireEvent(event); } } private void processAndSendResponse(Element element, Message message) { Element response = new Element(RESPONSE, Transport.NAMESPACE); XmlResponseProvider provider = XmlResponseProvider.getProvider(element, getBroadcaster()); if (provider.processAndFillResponse( response, element, JabberTransport.this, getFrom(message))) { Message responseMessage = new Message(getFrom(message)); responseMessage.addExtension(new JDOMExtension(response)); responseMessage.setThread(message.getThread()); myFacade.getConnection().sendPacket(responseMessage); } } private String getFrom(Message message) { return getSimpleId(message.getFrom()); } } private class MyReconnectRunnable implements Runnable { @Override public void run() { try { Thread.sleep(myReconnectTimeout); if (myFacade.connect() != null && myFacade.getMyAccount().isLoginAllowed()) { myReconnectProcess = myIdeFacade.runOnPooledThread(this); } } catch (InterruptedException ignored) { // return } } } }
/** * Returns the supported features by this XMPP entity. * * @return an Iterator on the supported features by this XMPP entity. */ public Iterator<String> getFeatures() { synchronized (features) { return Collections.unmodifiableList(new ArrayList<String>(features)).iterator(); } }
/** * Returns all identities of this client as unmodifiable Collection * * @return */ public Set<DiscoverInfo.Identity> getIdentities() { Set<Identity> res = new HashSet<Identity>(identities); // Add the default identity that must exist res.add(defaultIdentity); return Collections.unmodifiableSet(res); }
/** * Manages discovery of services in XMPP entities. This class provides: * * <ol> * <li>A registry of supported features in this XMPP entity. * <li>Automatic response when this XMPP entity is queried for information. * <li>Ability to discover items and information of remote XMPP entities. * <li>Ability to publish publicly available items. * </ol> * * @author Gaston Dombiak */ public class ServiceDiscoveryManager { private static final String DEFAULT_IDENTITY_NAME = "Smack"; private static final String DEFAULT_IDENTITY_CATEGORY = "client"; private static final String DEFAULT_IDENTITY_TYPE = "pc"; private static DiscoverInfo.Identity defaultIdentity = new Identity(DEFAULT_IDENTITY_CATEGORY, DEFAULT_IDENTITY_NAME, DEFAULT_IDENTITY_TYPE); private Set<DiscoverInfo.Identity> identities = new HashSet<DiscoverInfo.Identity>(); private DiscoverInfo.Identity identity = defaultIdentity; private EntityCapsManager capsManager; private static Map<Connection, ServiceDiscoveryManager> instances = Collections.synchronizedMap(new WeakHashMap<Connection, ServiceDiscoveryManager>()); private WeakReference<Connection> connection; private final Set<String> features = new HashSet<String>(); private DataForm extendedInfo = null; private Map<String, NodeInformationProvider> nodeInformationProviders = new ConcurrentHashMap<String, NodeInformationProvider>(); // Create a new ServiceDiscoveryManager on every established connection static { Connection.addConnectionCreationListener( new ConnectionCreationListener() { public void connectionCreated(Connection connection) { getInstanceFor(connection); } }); } /** * Set the default identity all new connections will have. If unchanged the default identity is an * identity where category is set to 'client', type is set to 'pc' and name is set to 'Smack'. * * @param identity */ public static void setDefaultIdentity(DiscoverInfo.Identity identity) { defaultIdentity = identity; } /** * Creates a new ServiceDiscoveryManager for a given Connection. This means that the service * manager will respond to any service discovery request that the connection may receive. * * @param connection the connection to which a ServiceDiscoveryManager is going to be created. */ private ServiceDiscoveryManager(Connection connection) { this.connection = new WeakReference<Connection>(connection); // Register the new instance and associate it with the connection instances.put(connection, this); addFeature(DiscoverInfo.NAMESPACE); addFeature(DiscoverItems.NAMESPACE); // Listen for disco#items requests and answer with an empty result PacketFilter packetFilter = new PacketTypeFilter(DiscoverItems.class); PacketListener packetListener = new PacketListener() { public void processPacket(Packet packet) { Connection connection = ServiceDiscoveryManager.this.connection.get(); if (connection == null) return; DiscoverItems discoverItems = (DiscoverItems) packet; // Send back the items defined in the client if the request is of type GET if (discoverItems != null && discoverItems.getType() == IQ.Type.GET) { DiscoverItems response = new DiscoverItems(); response.setType(IQ.Type.RESULT); response.setTo(discoverItems.getFrom()); response.setPacketID(discoverItems.getPacketID()); response.setNode(discoverItems.getNode()); // Add the defined items related to the requested node. Look for // the NodeInformationProvider associated with the requested node. NodeInformationProvider nodeInformationProvider = getNodeInformationProvider(discoverItems.getNode()); if (nodeInformationProvider != null) { // Specified node was found, add node items response.addItems(nodeInformationProvider.getNodeItems()); // Add packet extensions response.addExtensions(nodeInformationProvider.getNodePacketExtensions()); } else if (discoverItems.getNode() != null) { // Return <item-not-found/> error since client doesn't contain // the specified node response.setType(IQ.Type.ERROR); response.setError(new XMPPError(XMPPError.Condition.item_not_found)); } connection.sendPacket(response); } } }; connection.addPacketListener(packetListener, packetFilter); // Listen for disco#info requests and answer the client's supported features // To add a new feature as supported use the #addFeature message packetFilter = new PacketTypeFilter(DiscoverInfo.class); packetListener = new PacketListener() { public void processPacket(Packet packet) { Connection connection = ServiceDiscoveryManager.this.connection.get(); if (connection == null) return; DiscoverInfo discoverInfo = (DiscoverInfo) packet; // Answer the client's supported features if the request is of the GET type if (discoverInfo != null && discoverInfo.getType() == IQ.Type.GET) { DiscoverInfo response = new DiscoverInfo(); response.setType(IQ.Type.RESULT); response.setTo(discoverInfo.getFrom()); response.setPacketID(discoverInfo.getPacketID()); response.setNode(discoverInfo.getNode()); // Add the client's identity and features only if "node" is null // and if the request was not send to a node. If Entity Caps are // enabled the client's identity and features are may also added // if the right node is chosen if (discoverInfo.getNode() == null) { addDiscoverInfoTo(response); } else { // Disco#info was sent to a node. Check if we have information of the // specified node NodeInformationProvider nodeInformationProvider = getNodeInformationProvider(discoverInfo.getNode()); if (nodeInformationProvider != null) { // Node was found. Add node features response.addFeatures(nodeInformationProvider.getNodeFeatures()); // Add node identities response.addIdentities(nodeInformationProvider.getNodeIdentities()); // Add packet extensions response.addExtensions(nodeInformationProvider.getNodePacketExtensions()); } else { // Return <item-not-found/> error since specified node was not found response.setType(IQ.Type.ERROR); response.setError(new XMPPError(XMPPError.Condition.item_not_found)); } } connection.sendPacket(response); } } }; connection.addPacketListener(packetListener, packetFilter); } /** * Returns the name of the client that will be returned when asked for the client identity in a * disco request. The name could be any value you need to identity this client. * * @return the name of the client that will be returned when asked for the client identity in a * disco request. */ public String getIdentityName() { return identity.getName(); } /** * Sets the name of the client that will be returned when asked for the client identity in a disco * request. The name could be any value you need to identity this client. * * @param name the name of the client that will be returned when asked for the client identity in * a disco request. */ public void setIdentityName(String name) { identity.setName(name); renewEntityCapsVersion(); } /** * Returns the type of client that will be returned when asked for the client identity in a disco * request. The valid types are defined by the category client. Follow this link to learn the * possible types: <a * href="http://xmpp.org/registrar/disco-categories.html#client">Jabber::Registrar</a>. * * @return the type of client that will be returned when asked for the client identity in a disco * request. */ public String getIdentityType() { return identity.getType(); } /** * Sets the type of client that will be returned when asked for the client identity in a disco * request. The valid types are defined by the category client. Follow this link to learn the * possible types: <a * href="http://xmpp.org/registrar/disco-categories.html#client">Jabber::Registrar</a>. * * @param type the type of client that will be returned when asked for the client identity in a * disco request. */ @SuppressWarnings("deprecation") public void setIdentityType(String type) { identity.setType(type); renewEntityCapsVersion(); } /** * Add an identity to the client. * * @param identity */ public void addIdentity(DiscoverInfo.Identity identity) { identities.add(identity); renewEntityCapsVersion(); } /** * Remove an identity from the client. Note that the client needs at least one identity, the * default identity, which can not be removed. * * @param identity * @return true, if successful. Otherwise the default identity was given. */ public boolean removeIdentity(DiscoverInfo.Identity identity) { if (identity.equals(this.identity)) return false; identities.remove(identity); renewEntityCapsVersion(); return true; } /** * Returns all identities of this client as unmodifiable Collection * * @return */ public Set<DiscoverInfo.Identity> getIdentities() { Set<Identity> res = new HashSet<Identity>(identities); // Add the default identity that must exist res.add(defaultIdentity); return Collections.unmodifiableSet(res); } /** * Returns the ServiceDiscoveryManager instance associated with a given Connection. * * @param connection the connection used to look for the proper ServiceDiscoveryManager. * @return the ServiceDiscoveryManager associated with a given Connection. */ public static synchronized ServiceDiscoveryManager getInstanceFor(Connection connection) { ServiceDiscoveryManager sdm = instances.get(connection); if (sdm == null) { sdm = new ServiceDiscoveryManager(connection); } return sdm; } /** * Add discover info response data. * * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol; * Example 2</a> * @param response the discover info response packet */ public void addDiscoverInfoTo(DiscoverInfo response) { // First add the identities of the connection response.addIdentities(getIdentities()); // Add the registered features to the response synchronized (features) { for (Iterator<String> it = getFeatures(); it.hasNext(); ) { response.addFeature(it.next()); } response.addExtension(extendedInfo); } } /** * Returns the NodeInformationProvider responsible for providing information (ie items) related to * a given node or <tt>null</null> if none. * * <p>In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the * NodeInformationProvider will provide information about the rooms where the user has joined. * * @param node the node that contains items associated with an entity not addressable as a JID. * @return the NodeInformationProvider responsible for providing information related to a given * node. */ private NodeInformationProvider getNodeInformationProvider(String node) { if (node == null) { return null; } return nodeInformationProviders.get(node); } /** * Sets the NodeInformationProvider responsible for providing information (ie items) related to a * given node. Every time this client receives a disco request regarding the items of a given * node, the provider associated to that node will be the responsible for providing the requested * information. * * <p>In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the * NodeInformationProvider will provide information about the rooms where the user has joined. * * @param node the node whose items will be provided by the NodeInformationProvider. * @param listener the NodeInformationProvider responsible for providing items related to the * node. */ public void setNodeInformationProvider(String node, NodeInformationProvider listener) { nodeInformationProviders.put(node, listener); } /** * Removes the NodeInformationProvider responsible for providing information (ie items) related to * a given node. This means that no more information will be available for the specified node. * * <p>In MUC, a node could be 'http://jabber.org/protocol/muc#rooms' which means that the * NodeInformationProvider will provide information about the rooms where the user has joined. * * @param node the node to remove the associated NodeInformationProvider. */ public void removeNodeInformationProvider(String node) { nodeInformationProviders.remove(node); } /** * Returns the supported features by this XMPP entity. * * @return an Iterator on the supported features by this XMPP entity. */ public Iterator<String> getFeatures() { synchronized (features) { return Collections.unmodifiableList(new ArrayList<String>(features)).iterator(); } } /** * Returns the supported features by this XMPP entity. * * @return a copy of the List on the supported features by this XMPP entity. */ public List<String> getFeaturesList() { synchronized (features) { return new LinkedList<String>(features); } } /** * Registers that a new feature is supported by this XMPP entity. When this client is queried for * its information the registered features will be answered. * * <p>Since no packet is actually sent to the server it is safe to perform this operation before * logging to the server. In fact, you may want to configure the supported features before logging * to the server so that the information is already available if it is required upon login. * * @param feature the feature to register as supported. */ public void addFeature(String feature) { synchronized (features) { features.add(feature); renewEntityCapsVersion(); } } /** * Removes the specified feature from the supported features by this XMPP entity. * * <p>Since no packet is actually sent to the server it is safe to perform this operation before * logging to the server. * * @param feature the feature to remove from the supported features. */ public void removeFeature(String feature) { synchronized (features) { features.remove(feature); renewEntityCapsVersion(); } } /** * Returns true if the specified feature is registered in the ServiceDiscoveryManager. * * @param feature the feature to look for. * @return a boolean indicating if the specified featured is registered or not. */ public boolean includesFeature(String feature) { synchronized (features) { return features.contains(feature); } } /** * Registers extended discovery information of this XMPP entity. When this client is queried for * its information this data form will be returned as specified by XEP-0128. * * <p>Since no packet is actually sent to the server it is safe to perform this operation before * logging to the server. In fact, you may want to configure the extended info before logging to * the server so that the information is already available if it is required upon login. * * @param info the data form that contains the extend service discovery information. */ public void setExtendedInfo(DataForm info) { extendedInfo = info; renewEntityCapsVersion(); } /** * Returns the data form that is set as extended information for this Service Discovery instance * (XEP-0128) * * @see <a href="http://xmpp.org/extensions/xep-0128.html">XEP-128: Service Discovery * Extensions</a> * @return */ public DataForm getExtendedInfo() { return extendedInfo; } /** * Returns the data form as List of PacketExtensions, or null if no data form is set. This * representation is needed by some classes (e.g. EntityCapsManager, NodeInformationProvider) * * @return */ public List<PacketExtension> getExtendedInfoAsList() { List<PacketExtension> res = null; if (extendedInfo != null) { res = new ArrayList<PacketExtension>(1); res.add(extendedInfo); } return res; } /** * Removes the data form containing extended service discovery information from the information * returned by this XMPP entity. * * <p>Since no packet is actually sent to the server it is safe to perform this operation before * logging to the server. */ public void removeExtendedInfo() { extendedInfo = null; renewEntityCapsVersion(); } /** * Returns the discovered information of a given XMPP entity addressed by its JID. Use null as * entityID to query the server * * @param entityID the address of the XMPP entity or null. * @return the discovered information. * @throws XMPPException if the operation failed for some reason. */ public DiscoverInfo discoverInfo(String entityID) throws XMPPException { if (entityID == null) return discoverInfo(null, null); // Check if the have it cached in the Entity Capabilities Manager DiscoverInfo info = EntityCapsManager.getDiscoverInfoByUser(entityID); if (info != null) { // We were able to retrieve the information from Entity Caps and // avoided a disco request, hurray! return info; } // Try to get the newest node#version if it's known, otherwise null is // returned EntityCapsManager.NodeVerHash nvh = EntityCapsManager.getNodeVerHashByJid(entityID); // Discover by requesting the information from the remote entity // Note that wee need to use NodeVer as argument for Node if it exists info = discoverInfo(entityID, nvh != null ? nvh.getNodeVer() : null); // If the node version is known, store the new entry. if (nvh != null) { if (EntityCapsManager.verifyDiscoverInfoVersion(nvh.getVer(), nvh.getHash(), info)) EntityCapsManager.addDiscoverInfoByNode(nvh.getNodeVer(), info); } return info; } /** * Returns the discovered information of a given XMPP entity addressed by its JID and note * attribute. Use this message only when trying to query information which is not directly * addressable. * * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-basic">XEP-30 Basic Protocol</a> * @see <a href="http://xmpp.org/extensions/xep-0030.html#info-nodes">XEP-30 Info Nodes</a> * @param entityID the address of the XMPP entity. * @param node the optional attribute that supplements the 'jid' attribute. * @return the discovered information. * @throws XMPPException if the operation failed for some reason. */ public DiscoverInfo discoverInfo(String entityID, String node) throws XMPPException { Connection connection = ServiceDiscoveryManager.this.connection.get(); if (connection == null) throw new XMPPException("Connection instance already gc'ed"); // Discover the entity's info DiscoverInfo disco = new DiscoverInfo(); disco.setType(IQ.Type.GET); disco.setTo(entityID); disco.setNode(node); Packet result = connection.createPacketCollectorAndSend(disco).nextResultOrThrow(); return (DiscoverInfo) result; } /** * Returns the discovered items of a given XMPP entity addressed by its JID. * * @param entityID the address of the XMPP entity. * @return the discovered information. * @throws XMPPException if the operation failed for some reason. */ public DiscoverItems discoverItems(String entityID) throws XMPPException { return discoverItems(entityID, null); } /** * Returns the discovered items of a given XMPP entity addressed by its JID and note attribute. * Use this message only when trying to query information which is not directly addressable. * * @param entityID the address of the XMPP entity. * @param node the optional attribute that supplements the 'jid' attribute. * @return the discovered items. * @throws XMPPException if the operation failed for some reason. */ public DiscoverItems discoverItems(String entityID, String node) throws XMPPException { Connection connection = ServiceDiscoveryManager.this.connection.get(); if (connection == null) throw new XMPPException("Connection instance already gc'ed"); // Discover the entity's items DiscoverItems disco = new DiscoverItems(); disco.setType(IQ.Type.GET); disco.setTo(entityID); disco.setNode(node); Packet result = connection.createPacketCollectorAndSend(disco).nextResultOrThrow(); return (DiscoverItems) result; } /** * Returns true if the server supports publishing of items. A client may wish to publish items to * the server so that the server can provide items associated to the client. These items will be * returned by the server whenever the server receives a disco request targeted to the bare * address of the client (i.e. [email protected]). * * @param entityID the address of the XMPP entity. * @return true if the server supports publishing of items. * @throws XMPPException if the operation failed for some reason. */ public boolean canPublishItems(String entityID) throws XMPPException { DiscoverInfo info = discoverInfo(entityID); return canPublishItems(info); } /** * Returns true if the server supports publishing of items. A client may wish to publish items to * the server so that the server can provide items associated to the client. These items will be * returned by the server whenever the server receives a disco request targeted to the bare * address of the client (i.e. [email protected]). * * @param DiscoverInfo the discover info packet to check. * @return true if the server supports publishing of items. */ public static boolean canPublishItems(DiscoverInfo info) { return info.containsFeature("http://jabber.org/protocol/disco#publish"); } /** * Publishes new items to a parent entity. The item elements to publish MUST have at least a 'jid' * attribute specifying the Entity ID of the item, and an action attribute which specifies the * action being taken for that item. Possible action values are: "update" and "remove". * * @param entityID the address of the XMPP entity. * @param discoverItems the DiscoveryItems to publish. * @throws XMPPException if the operation failed for some reason. */ public void publishItems(String entityID, DiscoverItems discoverItems) throws XMPPException { publishItems(entityID, null, discoverItems); } /** * Publishes new items to a parent entity and node. The item elements to publish MUST have at * least a 'jid' attribute specifying the Entity ID of the item, and an action attribute which * specifies the action being taken for that item. Possible action values are: "update" and * "remove". * * @param entityID the address of the XMPP entity. * @param node the attribute that supplements the 'jid' attribute. * @param discoverItems the DiscoveryItems to publish. * @throws XMPPException if the operation failed for some reason. */ public void publishItems(String entityID, String node, DiscoverItems discoverItems) throws XMPPException { Connection connection = ServiceDiscoveryManager.this.connection.get(); if (connection == null) throw new XMPPException("Connection instance already gc'ed"); discoverItems.setType(IQ.Type.SET); discoverItems.setTo(entityID); discoverItems.setNode(node); connection.createPacketCollectorAndSend(discoverItems).nextResultOrThrow(); } /** Entity Capabilities */ /** * Loads the ServiceDiscoveryManager with an EntityCapsManger that speeds up certain lookups * * @param manager */ public void setEntityCapsManager(EntityCapsManager manager) { capsManager = manager; } /** Updates the Entity Capabilities Verification String if EntityCaps is enabled */ private void renewEntityCapsVersion() { if (capsManager != null && capsManager.entityCapsEnabled()) capsManager.updateLocalEntityCaps(); } }
/** * XMPP连接管理 * * @author JerSuen */ public class XmppManager extends IXmppManager.Stub { private XMPPConnection connection; private String account, password; private ConnectionListener connectionListener; private RosterListener rosterListener; private IMService imService; private PacketListener messageListener; private Map<String, Chat> jidChats = Collections.synchronizedMap(new HashMap<String, Chat>()); public XmppManager( ConnectionConfiguration config, String account, String password, IMService imService) { this(new XMPPTCPConnection(config), account, password, imService); } public XmppManager( XMPPConnection connection, String account, String password, IMService imService) { this.connection = connection; this.account = account; this.password = password; this.imService = imService; } /** 建立XMPP连接 */ public boolean connect() throws RemoteException { // 已经连接 if (connection.isConnected()) { return true; } else { try { // 开始连接 connection.connect(); if (connectionListener == null) { // 添加一个连接监听器 connectionListener = new IMClientConnectListener(); } connection.addConnectionListener(connectionListener); return true; } catch (SmackException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (XMPPException e) { e.printStackTrace(); } } return false; } /** 登陆XMPP服务器 */ public boolean login() throws RemoteException { // 未建立XMPP连接 if (!connection.isConnected()) { return false; } // 应经登陆过 if (connection.isAuthenticated()) { return true; } else { // 开始登陆 try { connection.login(account, password, imService.getString(R.string.app_name)); if (messageListener == null) { messageListener = new MessageListener(); } // 添加消息监听器 connection.addPacketListener(messageListener, new PacketTypeFilter(Message.class)); Roster roster = connection.getRoster(); if (rosterListener == null) { rosterListener = new IMClientRosterListener(); } // 添加花名册监听器 roster.addRosterListener(rosterListener); // 获取花名册 if (roster != null && roster.getEntries().size() > 0) { Uri uri = null; for (RosterEntry entry : roster.getEntries()) { // 获取联系人名片信息 VCard vCard = new VCard(); vCard.load(connection, entry.getUser()); // 用户名称 String userName = StringUtils.parseName(entry.getUser()); // 用户备注 String remarks = entry.getName(); // 通讯录的名称 String name = ""; // 名称与备注判断 if (userName.equals(remarks) && vCard != null) { // 使用联系人的昵称 name = vCard.getNickName(); } else { // 使用备注 name = remarks; } if (vCard != null) { IM.saveAvatar(vCard.getAvatar(), StringUtils.parseName(entry.getUser())); } ContentValues values = new ContentValues(); values.put(ContactsProvider.ContactColumns.ACCOUNT, entry.getUser()); values.put(ContactsProvider.ContactColumns.NAME, name); String sortStr = PinYin.getPinYin(name); values.put(ContactsProvider.ContactColumns.SORT, sortStr); values.put( ContactsProvider.ContactColumns.SECTION, sortStr.substring(0, 1).toUpperCase(Locale.ENGLISH)); // 储存联系人 if (imService .getContentResolver() .update( ContactsProvider.CONTACT_URI, values, ContactsProvider.ContactColumns.ACCOUNT + " = ?", new String[] {entry.getUser()}) == 0) { uri = imService.getContentResolver().insert(ContactsProvider.CONTACT_URI, values); } } // 发生改变,通知刷新 if (uri != null) { imService.getContentResolver().notifyChange(uri, null); } } } catch (XMPPException e) { e.printStackTrace(); } catch (SmackException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return true; } } /** 关闭XMPP连接 */ public boolean disconnect() throws RemoteException { if (connection != null && connection.isConnected()) { connection.disconnect(); } return true; } /** 发送消息 */ public void sendMessage(String sessionJID, String sessionName, String message, String type) throws RemoteException { ChatManager chatManager = ChatManager.getInstanceFor(connection); Chat chat; // 查找Chat对策 if (jidChats.containsKey(sessionJID)) { chat = jidChats.get(sessionJID); // 创建Chat } else { chat = chatManager.createChat(sessionJID, null); // 添加到集合 jidChats.put(sessionJID, chat); } if (chat != null) { try { // 发送消息 chat.sendMessage(message); // 保存聊天记录 ContentValues values = new ContentValues(); values.put(SMSProvider.SMSColumns.BODY, message); values.put(SMSProvider.SMSColumns.TYPE, type); values.put(SMSProvider.SMSColumns.TIME, System.currentTimeMillis()); values.put(SMSProvider.SMSColumns.WHO_ID, IM.getString(IM.ACCOUNT_JID)); values.put(SMSProvider.SMSColumns.SESSION_ID, sessionJID); values.put(SMSProvider.SMSColumns.SESSION_NAME, sessionName); imService.getContentResolver().insert(SMSProvider.SMS_URI, values); } catch (XMPPException e) { e.printStackTrace(); } catch (SmackException.NotConnectedException e) { e.printStackTrace(); } } } /** XMPP连接监听器 */ private class IMClientConnectListener implements ConnectionListener { public void connected(XMPPConnection connection) {} public void authenticated(XMPPConnection connection) {} public void connectionClosed() {} public void connectionClosedOnError(Exception e) {} public void reconnectingIn(int seconds) {} public void reconnectionSuccessful() {} public void reconnectionFailed(Exception e) {} } /** 花名册监听器 */ private class IMClientRosterListener implements RosterListener { public void entriesAdded(Collection<String> strings) {} public void entriesUpdated(Collection<String> strings) {} public void entriesDeleted(Collection<String> strings) {} public void presenceChanged(Presence presence) {} } /** 消息监听器 */ private class MessageListener implements PacketListener { public void processPacket(Packet packet) throws SmackException.NotConnectedException { if (packet instanceof Message) { Message message = (Message) packet; // 聊天消息 if (message.getType() == Message.Type.chat) { String whoAccountStr = StringUtils.parseBareAddress(message.getFrom()); String whoNameStr = ""; // 查询联系人的名称 Cursor cursor = imService .getContentResolver() .query( ContactsProvider.CONTACT_URI, null, ContactsProvider.ContactColumns.ACCOUNT + " = ?", new String[] {whoAccountStr}, null); if (cursor != null && cursor.moveToFirst()) { cursor.moveToPosition(0); whoNameStr = cursor.getString(cursor.getColumnIndex(ContactsProvider.ContactColumns.NAME)); } String bodyStr = message.getBody(); String typeStr = "chat"; // 插入消息 ContentValues values = new ContentValues(); values.put(SMSProvider.SMSColumns.BODY, bodyStr); values.put(SMSProvider.SMSColumns.TYPE, typeStr); values.put(SMSProvider.SMSColumns.TIME, System.currentTimeMillis()); values.put(SMSProvider.SMSColumns.WHO_ID, whoAccountStr); values.put(SMSProvider.SMSColumns.SESSION_ID, whoAccountStr); values.put(SMSProvider.SMSColumns.SESSION_NAME, whoNameStr); imService.getContentResolver().insert(SMSProvider.SMS_URI, values); } } } } }