/**
* Copyright (c) 2013, Redsolution LTD. All rights reserved.
*
* This file is part of Xabber project; you can redistribute it and/or
* modify it under the terms of the GNU General Public License, Version 3.
*
* Xabber is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License,
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.xabber.android.data.roster;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import com.xabber.android.R;
import com.xabber.android.data.Application;
import com.xabber.android.data.NetworkException;
import com.xabber.android.data.account.AccountItem;
import com.xabber.android.data.account.AccountManager;
import com.xabber.android.data.account.listeners.OnAccountDisabledListener;
import com.xabber.android.data.account.listeners.OnAccountEnabledListener;
import com.xabber.android.data.connection.ConnectionItem;
import com.xabber.android.data.connection.StanzaSender;
import com.xabber.android.data.connection.listeners.OnDisconnectListener;
import com.xabber.android.data.entity.AccountJid;
import com.xabber.android.data.entity.NestedMap;
import com.xabber.android.data.entity.UserJid;
import com.xabber.android.data.extension.muc.RoomChat;
import com.xabber.android.data.extension.muc.RoomContact;
import com.xabber.android.data.log.LogManager;
import com.xabber.android.data.message.AbstractChat;
import com.xabber.android.data.message.ChatContact;
import com.xabber.android.data.message.MessageManager;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterEntry;
import org.jivesoftware.smack.roster.packet.RosterPacket;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.Jid;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
/**
* Manage contact list (roster).
*
* @author alexander.ivanov
*/
public class RosterManager implements OnDisconnectListener, OnAccountEnabledListener,
OnAccountDisabledListener {
private static final String LOG_TAG = RosterManager.class.getSimpleName();
private static RosterManager instance;
private NestedMap<RosterContact> rosterContacts;
private final NestedMap<WeakReference<AbstractContact>> contactsCache;
private RosterManager() {
rosterContacts = new NestedMap<>();
contactsCache = new NestedMap<>();
}
public static RosterManager getInstance() {
if (instance == null) {
instance = new RosterManager();
}
return instance;
}
@Nullable
private Roster getRoster(AccountJid account) {
final AccountItem accountItem = AccountManager.getInstance().getAccount(account);
if (accountItem == null) {
return null;
}
return Roster.getInstanceFor(accountItem.getConnection());
}
@Nullable
public Presence getPresence(AccountJid account, UserJid user) {
final Roster roster = getRoster(account);
if (roster == null) {
return null;
} else {
return roster.getPresence(user.getJid().asBareJid());
}
}
public List<Presence> getPresences(AccountJid account, Jid user) {
final Roster roster = getRoster(account);
if (roster == null) {
return new ArrayList<>();
} else {
return roster.getAvailablePresences(user.asBareJid());
}
}
public Collection<RosterContact> getAccountRosterContacts(final AccountJid accountJid) {
return Collections.unmodifiableCollection(rosterContacts.getNested(accountJid.toString()).values());
}
public Collection<RosterContact> getAllContacts() {
return Collections.unmodifiableCollection(rosterContacts.values());
}
void onContactsAdded(AccountJid account, Collection<Jid> addresses) {
final Roster roster = RosterManager.getInstance().getRoster(account);
Collection<RosterContact> newContacts = new ArrayList<>(addresses.size());
for (Jid jid : addresses) {
RosterEntry entry = roster.getEntry(jid.asBareJid());
try {
RosterContact contact = convertRosterEntryToRosterContact(account, roster, entry);
rosterContacts.put(account.toString(),
contact.getUser().getBareJid().toString(), contact);
newContacts.add(contact);
} catch (UserJid.UserJidCreateException e) {
LogManager.exception(LOG_TAG, e);
}
}
onContactsChanged(newContacts);
}
void onContactsUpdated(AccountJid account, Collection<Jid> addresses) {
onContactsAdded(account, addresses);
}
void onContactsDeleted(AccountJid account, Collection<Jid> addresses) {
Collection<RosterContact> removedContacts = new ArrayList<>(addresses.size());
for (Jid jid : addresses) {
RosterContact contact = rosterContacts.remove(account.toString(), jid.asBareJid().toString());
if (contact != null) {
removedContacts.add(contact);
}
}
onContactsChanged(removedContacts);
}
@NonNull
private RosterContact convertRosterEntryToRosterContact(AccountJid account, Roster roster, RosterEntry rosterEntry) throws UserJid.UserJidCreateException {
final RosterContact contact = RosterContact
.getRosterContact(account, UserJid.from(rosterEntry.getJid()), rosterEntry.getName());
final Collection<org.jivesoftware.smack.roster.RosterGroup> groups = roster.getGroups();
contact.clearGroupReferences();
for (org.jivesoftware.smack.roster.RosterGroup group : groups) {
if (group.contains(rosterEntry)) {
contact.addGroupReference(new RosterGroupReference(new RosterGroup(account, group.getName())));
}
}
contact.setEnabled(true);
contact.setConnected(true);
return contact;
}
public AbstractContact getAbstractContact(@NonNull AccountJid accountJid, @NonNull UserJid userJid) {
WeakReference<AbstractContact> contactWeakReference = contactsCache.get(accountJid.toString(), userJid.toString());
if (contactWeakReference != null && contactWeakReference.get() != null) {
return contactWeakReference.get();
}
AbstractContact newContact = new AbstractContact(accountJid, userJid);
contactsCache.put(accountJid.toString(), userJid.toString(), new WeakReference<>(newContact));
return newContact;
}
@Nullable
public RosterContact getRosterContact(AccountJid accountJid, BareJid bareJid) {
return rosterContacts.get(accountJid.toString(), bareJid.toString());
}
@Nullable
public RosterContact getRosterContact(AccountJid accountJid, UserJid userJid) {
return getRosterContact(accountJid, userJid.getBareJid());
}
/**
* Gets {@link RoomContact}, {@link RosterContact}, {@link ChatContact} or
* creates new {@link ChatContact}.
*
* @param account
* @param user
* @return
*/
public AbstractContact getBestContact(AccountJid account, UserJid user) {
AbstractChat abstractChat = MessageManager.getInstance().getChat(account, user);
if (abstractChat != null && abstractChat instanceof RoomChat) {
return new RoomContact((RoomChat) abstractChat);
}
RosterContact rosterContact = getRosterContact(account, user);
if (rosterContact != null) {
return rosterContact;
}
if (abstractChat != null) {
return new ChatContact(abstractChat);
}
return new ChatContact(account, user);
}
/**
* @param account
* @return List of groups in specified account.
*/
public Collection<String> getGroups(AccountJid account) {
final Roster roster = getRoster(account);
Collection<String> returnGroups = new ArrayList<>();
if (roster == null) {
return returnGroups;
}
final Collection<org.jivesoftware.smack.roster.RosterGroup> groups = roster.getGroups();
for (org.jivesoftware.smack.roster.RosterGroup rosterGroup : groups) {
returnGroups.add(rosterGroup.getName());
}
return returnGroups;
}
/**
* @return Contact's name.
*/
public String getName(AccountJid account, UserJid user) {
RosterContact contact = getRosterContact(account, user);
if (contact == null) {
return user.toString();
}
return contact.getName();
}
/**
* @return Contact's groups.
*/
public Collection<String> getGroups(AccountJid account, UserJid user) {
RosterContact contact = getRosterContact(account, user);
if (contact == null) {
return Collections.emptyList();
}
return contact.getGroupNames();
}
/**
* Requests to create new contact.
*
* @param account
* @param user
* @param name
* @param groups
* @throws NetworkException
*/
public void createContact(AccountJid account, UserJid user, String name,
Collection<String> groups)
throws SmackException.NotLoggedInException, XMPPException.XMPPErrorException,
SmackException.NotConnectedException, InterruptedException, SmackException.NoResponseException {
final Roster roster = getRoster(account);
if (roster == null) {
return;
}
if (user.getBareJid() != null) {
roster.createEntry(user.getBareJid(), name, groups.toArray(new String[groups.size()]));
}
}
/**
* Requests contact removing.
*
*/
public void removeContact(AccountJid account, UserJid user) {
final Roster roster = getRoster(account);
if (roster == null) {
return;
}
final RosterEntry entry = roster.getEntry(user.getJid().asBareJid());
if (entry == null) {
return;
}
Application.getInstance().runInBackgroundUserRequest(new Runnable() {
@Override
public void run() {
try {
roster.removeEntry(entry);
} catch (SmackException.NotLoggedInException | SmackException.NotConnectedException e) {
Application.getInstance().onError(R.string.NOT_CONNECTED);
} catch (SmackException.NoResponseException e) {
Application.getInstance().onError(R.string.CONNECTION_FAILED);
} catch (XMPPException.XMPPErrorException e) {
Application.getInstance().onError(R.string.XMPP_EXCEPTION);
} catch (InterruptedException e) {
LogManager.exception(LOG_TAG, e);
}
}
});
}
public void setGroups(AccountJid account, UserJid user, Collection<String> groups) throws NetworkException {
final Roster roster = getRoster(account);
if (roster == null) {
return;
}
final RosterEntry entry = roster.getEntry(user.getJid().asBareJid());
if (entry == null) {
return;
}
RosterPacket packet = new RosterPacket();
packet.setType(IQ.Type.set);
RosterPacket.Item item = new RosterPacket.Item(user.getBareJid(), entry.getName());
for (String group : groups) {
item.addGroupName(group);
}
packet.addRosterItem(item);
StanzaSender.sendStanza(account, packet);
}
public void setName(AccountJid account, UserJid user, final String name) {
final Roster roster = getRoster(account);
if (roster == null) {
return;
}
final RosterEntry entry = roster.getEntry(user.getJid().asBareJid());
if (entry == null) {
return;
}
try {
entry.setName(name.trim());
} catch (SmackException.NotConnectedException e) {
Application.getInstance().onError(R.string.NOT_CONNECTED);
} catch (SmackException.NoResponseException e) {
Application.getInstance().onError(R.string.CONNECTION_FAILED);
} catch (XMPPException.XMPPErrorException e) {
Application.getInstance().onError(R.string.XMPP_EXCEPTION);
} catch (InterruptedException e) {
LogManager.exception(LOG_TAG, e);
}
}
/**
* Requests to remove group from all contacts in account.
*/
public void removeGroup(AccountJid account, String groupName)
throws NetworkException {
final Roster roster = getRoster(account);
if (roster == null) {
return;
}
final org.jivesoftware.smack.roster.RosterGroup group = roster.getGroup(groupName);
if (group == null) {
return;
}
Application.getInstance().runInBackgroundUserRequest(new Runnable() {
@Override
public void run() {
for (RosterEntry entry : group.getEntries()) {
try {
group.removeEntry(entry);
} catch (SmackException.NoResponseException e) {
Application.getInstance().onError(R.string.CONNECTION_FAILED);
} catch (SmackException.NotConnectedException e) {
Application.getInstance().onError(R.string.NOT_CONNECTED);
} catch (XMPPException.XMPPErrorException e) {
Application.getInstance().onError(R.string.XMPP_EXCEPTION);
} catch (InterruptedException e) {
LogManager.exception(LOG_TAG, e);
}
}
}
});
}
/**
* Requests to remove group from all contacts in all accounts.
*
*/
public void removeGroup(String group) throws NetworkException {
for (AccountJid account : AccountManager.getInstance().getEnabledAccounts()) {
removeGroup(account, group);
}
}
/**
* Requests to rename group.
*
* @param account
* @param oldGroup can be <code>null</code> for "no group".
*/
public void renameGroup(AccountJid account, String oldGroup, final String newGroup) {
if (newGroup.equals(oldGroup)) {
return;
}
final Roster roster = getRoster(account);
if (roster == null) {
return;
}
if (TextUtils.isEmpty(oldGroup)) {
Application.getInstance().runInBackgroundUserRequest(new Runnable() {
@Override
public void run() {
createGroupForUnfiledEntries(newGroup, roster);
}
});
return;
}
final org.jivesoftware.smack.roster.RosterGroup group = roster.getGroup(oldGroup);
if (group == null) {
return;
}
Application.getInstance().runInBackgroundUserRequest(new Runnable() {
@Override
public void run() {
try {
group.setName(newGroup);
} catch (SmackException.NoResponseException e) {
Application.getInstance().onError(R.string.CONNECTION_FAILED);
} catch (SmackException.NotConnectedException e) {
Application.getInstance().onError(R.string.NOT_CONNECTED);
} catch (XMPPException.XMPPErrorException e) {
Application.getInstance().onError(R.string.XMPP_EXCEPTION);
} catch (InterruptedException e) {
LogManager.exception(LOG_TAG, e);
}
}
});
}
private void createGroupForUnfiledEntries(String newGroup, Roster roster) {
final Set<RosterEntry> unfiledEntries = roster.getUnfiledEntries();
final org.jivesoftware.smack.roster.RosterGroup group = roster.createGroup(newGroup);
try {
for (RosterEntry entry : unfiledEntries) {
group.addEntry(entry);
}
} catch (SmackException.NoResponseException e) {
Application.getInstance().onError(R.string.CONNECTION_FAILED);
} catch (SmackException.NotConnectedException e) {
Application.getInstance().onError(R.string.NOT_CONNECTED);
} catch (XMPPException.XMPPErrorException e) {
Application.getInstance().onError(R.string.XMPP_EXCEPTION);
} catch (InterruptedException e) {
LogManager.exception(LOG_TAG, e);
}
}
/**
* Requests to rename group from all accounts.
*
* @param oldGroup can be <code>null</code> for "no group".
*/
public void renameGroup(String oldGroup, String newGroup) {
for (AccountJid account : AccountManager.getInstance().getEnabledAccounts()) {
renameGroup(account, oldGroup, newGroup);
}
}
/**
* @param account
* @return Whether roster for specified account has been received.
*/
public boolean isRosterReceived(AccountJid account) {
final Roster roster = getRoster(account);
return roster != null && roster.isLoaded();
}
@Override
public void onDisconnect(ConnectionItem connection) {
if (!(connection instanceof AccountItem)) {
return;
}
Collection<RosterContact> accountContacts
= rosterContacts.getNested(connection.getAccount().toString()).values();
for (RosterContact contact : accountContacts) {
contact.setConnected(false);
}
}
@Override
public void onAccountEnabled(AccountItem accountItem) {
setEnabled(accountItem.getAccount(), true);
}
@Override
public void onAccountDisabled(AccountItem accountItem) {
setEnabled(accountItem.getAccount(), false);
}
/**
* Sets whether contacts in accounts are enabled.
*/
private void setEnabled(AccountJid account, boolean enabled) {
Collection<RosterContact> accountContacts
= rosterContacts.getNested(account.toString()).values();
for (RosterContact contact : accountContacts) {
contact.setEnabled(enabled);
}
}
/**
* Notifies registered {@link OnContactChangedListener}.
*
* @param entities
*/
public static void onContactsChanged(final Collection<RosterContact> entities) {
Application.getInstance().runOnUiThread(new Runnable() {
@Override
public void run() {
for (OnContactChangedListener onContactChangedListener : Application
.getInstance().getUIListeners(OnContactChangedListener.class)) {
onContactChangedListener.onContactsChanged(entities);
}
}
});
}
/**
* Notifies registered {@link OnContactChangedListener}.
*/
public static void onContactChanged(AccountJid account, UserJid bareAddress) {
final Collection<RosterContact> entities = new ArrayList<>();
RosterContact rosterContact = getInstance().getRosterContact(account, bareAddress);
if (rosterContact != null) {
entities.add(rosterContact);
}
onContactsChanged(entities);
}
}