/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Distributable under LGPL license. See terms of license at gnu.org.
 */
package net.java.sip.communicator.impl.protocol.jabber;

import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

import net.java.sip.communicator.impl.protocol.jabber.OperationSetPersistentPresenceJabberImpl.ContactChangesListener;
import net.java.sip.communicator.service.contactlist.MetaContactGroup;
import net.java.sip.communicator.service.customavatar.CustomAvatarService;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;

import org.atalk.android.plugin.timberlog.TimberLog;
import org.atalk.persistance.DatabaseBackend;
import org.jivesoftware.smack.SmackException.*;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.filter.PresenceTypeFilter;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smack.packet.StanzaError.Condition;
import org.jivesoftware.smack.roster.*;
import org.jivesoftware.smackx.avatar.AvatarManager;
import org.jivesoftware.smackx.avatar.vcardavatar.VCardAvatarManager;
import org.jivesoftware.smackx.nick.packet.Nick;
import org.jxmpp.jid.*;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.jid.parts.Localpart;
import org.jxmpp.stringprep.XmppStringprepException;
import org.osgi.framework.ServiceReference;

import java.util.*;

import timber.log.Timber;

import static org.jivesoftware.smack.roster.packet.RosterPacket.ItemType.both;
import static org.jivesoftware.smack.roster.packet.RosterPacket.ItemType.from;
import static org.jivesoftware.smack.roster.packet.RosterPacket.ItemType.none;
import static org.jivesoftware.smack.roster.packet.RosterPacket.ItemType.to;


/**
 * This class encapsulates the Roster class. Once created, it will register itself as a listener to
 * the encapsulated Roster and modify its local copy of Contacts and ContactGroups every time an
 * event is generated by the underlying framework. The class would also generate corresponding
 * sip-communicator events to all events coming from smack.
 *
 * @author Damian Minkov
 * @author Emil Ivov
 * @author Hristo Terezov
 * @author Eng Chong Meng
 */
public class ServerStoredContactListJabberImpl
{
    /**
     * The jabber list that we encapsulate
     */
    private Roster mRoster = null;

    /**
     * The root {@code ContactGroup}. The container for all jabber buddies and groups.
     */
    private final RootContactGroupJabberImpl rootGroup;

    /**
     * The operation set that created us and that we could use when dispatching subscription events.
     */
    private final OperationSetPersistentPresenceJabberImpl parentOperationSet;

    /**
     * The provider that is on top of us.
     */
    private final ProtocolProviderServiceJabberImpl mPPS;

    /**
     * For multiple accounts support, RootGroup can be owned by any of pps at instance to add contact
     */
    private ProtocolProviderService rootGroupPPS;

    /**
     * Listeners that would receive event notifications for changes in group names or other
     * properties, removal or creation of groups.
     */
    private final Vector<ServerStoredGroupListener> serverStoredGroupListeners = new Vector<>();

    /**
     * Thread retrieving images for contacts
     */
    private ImageRetriever imageRetriever = null;

    /**
     * Listens for roster changes.
     */
    private ChangeListener rosterChangeListener = null;

    /**
     * Retrieve contact information.
     */
    private InfoRetriever infoRetriever;

    /*
     * Disable info Retrieval on first login even when local cache is empty
     * cmeng: 20190212seems ejabberd will send VCardTempXUpdate with photo attr in <presence/>
     */
    private boolean infoRetrieveOnStart = false;

    /**
     * Whether roster has been requested and dispatched.
     */
    private boolean isRosterInitialized = false;

    /**
     * Lock object for the isRosterInitialized variable.
     */
    private final Object rosterInitLock = new Object();

    /**
     * The initial status saved.
     */
    private PresenceStatus initialStatus = null;

    /**
     * The initial status message saved.
     */
    private String initialStatusMessage = null;

    private XMPPConnection xmppConnection = null;

    /**
     * Creates a ServerStoredContactList wrapper for the specified BuddyList.
     *
     * @param parentOperationSet the operation set that created us and
     * that we could use for dispatching subscription events
     * @param provider the provider that has instantiated us.
     * @param infoRetriever retrieve contact information.
     */
    ServerStoredContactListJabberImpl(OperationSetPersistentPresenceJabberImpl parentOperationSet,
            ProtocolProviderServiceJabberImpl provider, InfoRetriever infoRetriever)
    {
        // We need to init these as early as possible to ensure that the provider and the
        // operationsSet would not be null in the incoming events.
        this.parentOperationSet = parentOperationSet;
        this.mPPS = provider;
        this.rootGroup = new RootContactGroupJabberImpl(this.mPPS);
        this.infoRetriever = infoRetriever;
    }

    /**
     * Returns the base of the root group i.e. ContactGroup.
     *
     * @return the root ContactGroup for the ContactList
     */
    public ContactGroup getRootGroup()
    {
        return rootGroup;
    }

    /**
     * Returns the roster entry associated with the given XMPP address or
     * <code>null</code> if the user is not an entry in the roster.
     *
     * @param userJid the XMPP address of the user (e.g. "jsmith@example.com"). The address could be in any
     * valid format (e.g. "domain/resource", "user@domain" or "user@domain/resource").
     * @return the roster entry or <code>null</code> if it does not exist.
     */
    RosterEntry getRosterEntry(BareJid userJid)
    {
        return ((mRoster == null) || (userJid == null)) ? null : mRoster.getEntry(userJid);
    }

    /**
     * Returns the roster group with the specified name, or <code>null</code> if the group doesn't exist.
     *
     * @param name the name of the group.
     * @return the roster group with the specified name.
     */
    RosterGroup getRosterGroup(String name)
    {
        return mRoster.getGroup(name);
    }

    /**
     * Registers the specified group listener so that it would receive events on group
     * modification/creation/destruction.
     *
     * @param listener the ServerStoredGroupListener to register for group events
     */
    void addGroupListener(ServerStoredGroupListener listener)
    {
        synchronized (serverStoredGroupListeners) {
            if (!serverStoredGroupListeners.contains(listener))
                this.serverStoredGroupListeners.add(listener);
        }
    }

    /**
     * Removes the specified group listener so that it won't receive further events on group
     * modification/creation/destruction.
     *
     * @param listener the ServerStoredGroupListener to unregister
     */
    void removeGroupListener(ServerStoredGroupListener listener)
    {
        synchronized (serverStoredGroupListeners) {
            this.serverStoredGroupListeners.remove(listener);
        }
    }

    /**
     * Creates the corresponding event and notifies all <code>ServerStoredGroupListener</code>s that
     * the source group has been removed, changed, renamed or whatever happened to it.
     *
     * @param group the ContactGroup that has been created/modified/removed
     * i.e. RootContactGroupJabberImpl or ContactGroupJabberImpl
     * @param eventID the id of the event to generate.
     */
    private void fireGroupEvent(ContactGroup group, int eventID)
    {
        // bail out if no one's listening
        if (parentOperationSet == null) {
            Timber.d("No presence opSet available. Bailing out.");
            return;
        }

        ServerStoredGroupEvent evt = new ServerStoredGroupEvent(group, eventID,
                parentOperationSet.getServerStoredContactListRoot(), mPPS, parentOperationSet);
        Timber.log(TimberLog.FINER, "Will dispatch the following grp event: %s", evt);

        Iterable<ServerStoredGroupListener> listeners;
        synchronized (serverStoredGroupListeners) {
            listeners = new ArrayList<>(serverStoredGroupListeners);
        }

        /*
         * Sometimes contact statuses are received before the groups and contacts are being
         * created. This is a problem when we don't have already created unresolved contacts.
         * So we will check contact statuses to be sure they are correct.
         */
        if (eventID == ServerStoredGroupEvent.GROUP_CREATED_EVENT) {
            Iterator<Contact> iter = group.contacts();
            while (iter.hasNext()) {
                ContactJabberImpl c = (ContactJabberImpl) iter.next();

                // roster can be null, receiving system messages from server before we are log in
                if (mRoster != null) {
                    parentOperationSet.firePresenceStatusChanged(mRoster.getPresence(c.getJid().asBareJid()));
                }
            }
        }

        for (ServerStoredGroupListener listener : listeners) {
            if (eventID == ServerStoredGroupEvent.GROUP_REMOVED_EVENT)
                listener.groupRemoved(evt);
            else if (eventID == ServerStoredGroupEvent.GROUP_RENAMED_EVENT)
                listener.groupNameChanged(evt);
            else if (eventID == ServerStoredGroupEvent.GROUP_CREATED_EVENT)
                listener.groupCreated(evt);
            else if (eventID == ServerStoredGroupEvent.GROUP_RESOLVED_EVENT)
                listener.groupResolved(evt);
        }
    }

    /**
     * Make the parent persistent presence operation set dispatch a contact removed event.
     *
     * @param parentGroup the group where that the removed contact belonged to.
     * @param contact the contact that was removed.
     */
    void fireContactRemoved(ContactGroup parentGroup, ContactJabberImpl contact)
    {
        // bail out if no one's listening
        if (parentOperationSet == null) {
            Timber.d("No presence opSet available. Bailing out.");
            return;
        }
        Timber.log(TimberLog.FINER, "Removing %s from %s", contact.getAddress(), parentGroup.getGroupName());

        // dispatch
        parentOperationSet.fireSubscriptionEvent(contact, parentGroup, SubscriptionEvent.SUBSCRIPTION_REMOVED);
    }

    /**
     * Make the parent persistent presence operation set dispatch a subscription moved event.
     *
     * @param oldParentGroup the group where the source contact was located before being moved
     * @param newParentGroup the group that the source contact is currently in.
     * @param contact the contact that was added
     */
    private void fireContactMoved(ContactGroup oldParentGroup, ContactGroup newParentGroup, ContactJabberImpl contact)
    {
        // bail out if no one's listening
        if (parentOperationSet == null) {
            Timber.d("No presence opSet available. Bailing out.");
            return;
        }
        // dispatch
        parentOperationSet.fireSubscriptionMovedEvent(contact, oldParentGroup, newParentGroup);
    }

    /**
     * Returns a reference to the provider that created us.
     *
     * @return a reference to a ProtocolProviderServiceImpl instance.
     */
    ProtocolProviderServiceJabberImpl getParentProvider()
    {
        return mPPS;
    }

    /**
     * Returns the ContactGroup with the specified name or null if no such group was found.
     *
     * @param groupName the name of the group we're looking for.
     * @return a reference to the ContactGroupJabberImpl instance we're looking for or null if no such group was found.
     */
    public ContactGroupJabberImpl findContactGroup(String groupName)
    {
        // make sure we ignore any whitespaces
        groupName = groupName.trim();

        Iterator<ContactGroup> contactGroups = rootGroup.subgroups();
        while (contactGroups.hasNext()) {
            ContactGroupJabberImpl contactGroup = (ContactGroupJabberImpl) contactGroups.next();
            if (contactGroup.getGroupName().trim().equals(groupName))
                return contactGroup;
        }
        return null;
    }

    /**
     * Find a group with the specified Copy of Name. Used to track when a group name has changed
     *
     * @param groupName String
     * @return a reference to the ContactGroup instance we're looking for or null if no such group was found.
     */
    private ContactGroupJabberImpl findContactGroupByNameCopy(String groupName)
    {
        // make sure we ignore any whitespaces
        groupName = groupName.trim();

        Iterator<ContactGroup> contactGroups = rootGroup.subgroups();
        while (contactGroups.hasNext()) {
            ContactGroupJabberImpl contactGroup = (ContactGroupJabberImpl) contactGroups.next();
            if ((contactGroup.getNameCopy() != null) && contactGroup.getNameCopy().trim().equals(groupName))
                return contactGroup;
        }
        return null;
    }

    /**
     * Returns the Contact with the specified id or null if no such id was found.
     *
     * @param id the contactJid of the contact to find (BareJid in actual search).
     * @return the <code>Contact</code> carrying the specified <code>screenName</code> or <code>null</code> if
     * no such contact exits.
     */
    public ContactJabberImpl findContactById(Jid id)
    {
        if (id == null)
            return null;

        Iterator<ContactGroup> contactGroups = rootGroup.subgroups();
        ContactJabberImpl contact;

        while (contactGroups.hasNext()) {
            ContactGroupJabberImpl contactGroup = (ContactGroupJabberImpl) contactGroups.next();
            contact = contactGroup.findContact(id);
            if (contact != null)
                return contact;
        }

        // check for private contacts
        ContactGroupJabberImpl volatileGroup = getNonPersistentGroup();
        if (volatileGroup != null) {
            contact = volatileGroup.findContact(id);
            if (contact != null)
                return contact;
        }

        // try the root group for non-group contact
        return rootGroup.findContact(id);
    }

    /**
     * Returns the ContactGroup containing the specified contact or null if no such group or contact exist.
     *
     * @param child the contact whose parent group we're looking for.
     * @return the <code>ContactGroup</code> containing the specified <code>contact</code> or <code>null</code>
     * if no such group or contact exist.
     */
    public ContactGroup findContactGroup(ContactJabberImpl child)
    {
        Iterator<ContactGroup> contactGroups = rootGroup.subgroups();
        Jid contactJid = child.getJid();

        while (contactGroups.hasNext()) {
            ContactGroupJabberImpl contactGroup = (ContactGroupJabberImpl) contactGroups.next();

            if (contactGroup.findContact(contactJid) != null)
                return contactGroup;
        }

        // try the root group for non-grouped contact
        if (rootGroup.findContact(contactJid) != null)
            return rootGroup;

        return null;
    }

    /**
     * Adds a new contact with the specified screenName to the contactList in rootGroup.
     *
     * @param id the id of the contact to add.
     * @param pps the pps requesting for contact to add.
     * @throws OperationFailedException OperationFailedException, XmppStringprepException
     */
    public void addContact(ProtocolProviderService pps, String id)
            throws OperationFailedException
    {
        rootGroupPPS = pps;
        addContact(getRootGroup(), id);
    }

    /**
     * Adds a new contact with the specified screenName to the list under the specified group.
     * Also include StanzaListener to intercept <presence type='subscribe'/> to add support for
     * XEP-0172: User Nickname extension for this contact.
     *
     * @param id the id of the contact to add.
     * @param parent the group under which we want the new contact placed.
     * @throws OperationFailedException if the contact already exist
     */
    public void addContact(final ContactGroup parent, String id)
            throws OperationFailedException
    {
        Timber.log(TimberLog.FINER, "Adding contact %s to parent = %s", id, parent);
        final BareJid contactJid = parseAddressString(id);

        // if the contact is already in the contact list and is not volatile, then only broadcast an event
        // Should also check new owner against existing old owner, not just bareJid.
        String accountUuid = null;
        String mcGroupName = null;
        String[] parentNames = null;
        if (parent != null) {
            mcGroupName = parent.getGroupName();

            if (parent == getRootGroup()) {
                accountUuid = rootGroupPPS.getAccountID().getAccountUuid();
            }
            else {
                accountUuid = parent.getProtocolProvider().getAccountID().getAccountUuid();
                parentNames = new String[]{mcGroupName};
            }
        }

        String[] args = {accountUuid, mcGroupName, contactJid.toString()};
        SQLiteDatabase mDB = DatabaseBackend.getWritableDB();
        Cursor cursor = mDB.query(MetaContactGroup.TBL_CHILD_CONTACTS, null,
                MetaContactGroup.ACCOUNT_UUID + "=? AND " + MetaContactGroup.PROTO_GROUP_UID
                        + "=? AND " + MetaContactGroup.CONTACT_JID + "=?", args, null, null, null);
        if (cursor.getCount() > 0) {
            cursor.close();

            Timber.w("Contact %s already exists in group %s", contactJid, findContactGroup(contactJid.toString()));
            throw new OperationFailedException("Contact " + contactJid + " already exist",
                    OperationFailedException.SUBSCRIPTION_ALREADY_EXISTS);
        }
        cursor.close();

        // @see <a href="https://xmpp.org/extensions/xep-0172.html">XEP-0172: User Nickname</a>
        xmppConnection.addPresenceInterceptor(presenceBuilder -> {
            Presence presence = presenceBuilder.build();
            if (presence.getTo().isParentOf(contactJid)) {
                Nick nicknameExt = new Nick(JabberActivator.getGlobalDisplayDetailsService().getDisplayName(mPPS));
                presenceBuilder.addExtension(nicknameExt);

                // cmeng - End the listener once job is completed
                // xmppConnection.removePresenceInterceptor(this);
            }
            Timber.w("Presence subscribe for: %s", contactJid);
        }, PresenceTypeFilter.SUBSCRIBE::accept);

        /* Creates a new roster entry and presence subscription. The server will asynchronously
         * update the roster with the subscription status.
         */
        try {
            mRoster.createItemAndRequestSubscription(contactJid, contactJid.toString(), parentNames);
        } catch (XMPPErrorException ex) {
            String errTxt = "Error adding new jabber roster entry";
            Timber.e(ex, "%s", errTxt);
            int errorCode = OperationFailedException.INTERNAL_ERROR;
            StanzaError error = ex.getStanzaError();
            if (error != null) {
                switch (error.getCondition()) {
                    case forbidden:
                    case not_allowed:
                    case not_authorized:
                        errorCode = OperationFailedException.FORBIDDEN;
                        break;
                    default:
                        errorCode = OperationFailedException.INTERNAL_SERVER_ERROR;
                }
                errTxt = error.getDescriptiveText();
            }
            throw new OperationFailedException(errTxt, errorCode, ex);
        } catch (NotLoggedInException | NoResponseException | NotConnectedException | InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Creates a non persistent contact for the specified address. This would also create (if
     * necessary) a group for volatile contacts that would not be added to the server stored
     * contact list. This method would have no effect on the server stored contact list.
     *
     * @param id the address of the contact to create.
     * @param isPrivateMessagingContact indicates if the contact should be private messaging contact or not.
     * @param displayName the display name of the contact
     * @return the newly created volatile <code>ContactImpl</code>
     */
    ContactJabberImpl createVolatileContact(Jid id, boolean isPrivateMessagingContact, String displayName)
    {
        // Timber.w(new Exception(), "Create volatile contact: %s (%s)", id, displayName);
        VolatileContactJabberImpl newVolatileContact
                = new VolatileContactJabberImpl(id, this, isPrivateMessagingContact, displayName);

        // Check whether a volatile group already exists and if not create one
        ContactGroupJabberImpl theVolatileGroup = getNonPersistentGroup();

        // if the parent group is null then add necessary to create the group
        if (theVolatileGroup == null) {
            theVolatileGroup = new VolatileContactGroupJabberImpl(ContactGroup.VOLATILE_GROUP, this);

            theVolatileGroup.addContact(newVolatileContact);
            this.rootGroup.addSubGroup(theVolatileGroup);
            fireGroupEvent(theVolatileGroup, ServerStoredGroupEvent.GROUP_CREATED_EVENT);
        }
        else {
            theVolatileGroup.addContact(newVolatileContact);
            fireContactAdded(theVolatileGroup, newVolatileContact);
        }
        return newVolatileContact;
    }

    /**
     * Checks if the contact address is associated with private messaging contact or not.
     *
     * @param contactJid the address of the contact.
     * @return <code>true</code> the contact address is associated with private messaging contact and <code>false</code> if not.
     */
    public boolean isPrivateMessagingContact(Jid contactJid)
    {
        ContactGroupJabberImpl theVolatileGroup = getNonPersistentGroup();
        if (theVolatileGroup == null)
            return false;
        ContactJabberImpl contact = theVolatileGroup.findContact(contactJid);
        if (!(contact instanceof VolatileContactJabberImpl))
            return false;
        return ((VolatileContactJabberImpl) contact).isPrivateMessagingContact();
    }

    /**
     * Creates a non resolved contact for the specified address and inside the specified group. The
     * newly created contact would be added to the local contact list as a standard contact but
     * when an event is received from the server concerning this contact, then it will be reused
     * and only its isResolved field would be updated instead of creating the whole contact again.
     *
     * @param parentGroup the group where the unresolved contact is to be created
     * @param id the Address of the contact to create.
     * @return the newly created unresolved <code>ContactImpl</code>
     */
    synchronized ContactJabberImpl createUnresolvedContact(ContactGroup parentGroup, Jid id)
    {
        ContactJabberImpl existingContact = findContactById(id);
        if (existingContact != null) {
            return existingContact;
        }

        ContactJabberImpl newUnresolvedContact = new ContactJabberImpl(id, this, true);

        if (parentGroup instanceof ContactGroupJabberImpl)
            ((ContactGroupJabberImpl) parentGroup).addContact(newUnresolvedContact);
        else if (parentGroup instanceof RootContactGroupJabberImpl)
            ((RootContactGroupJabberImpl) parentGroup).addContact(newUnresolvedContact);

        fireContactAdded(parentGroup, newUnresolvedContact);
        return newUnresolvedContact;
    }

    /**
     * Creates a non resolved contact group for the specified name. The newly created group would
     * be added to the local contact list as any other group but when an event is received from the
     * server concerning this group, then it will be reused and only its isResolved field would be
     * updated instead of creating the whole group again.
     *
     * @param groupName the name of the group to create.
     * @return the newly created unresolved <code>ContactGroupImpl</code>
     */
    synchronized ContactGroupJabberImpl createUnresolvedContactGroup(String groupName)
    {
        ContactGroupJabberImpl existingGroup = findContactGroup(groupName);
        if (existingGroup != null) {
            return existingGroup;
        }

        ContactGroupJabberImpl newUnresolvedGroup = new ContactGroupJabberImpl(groupName, this);
        this.rootGroup.addSubGroup(newUnresolvedGroup);
        fireGroupEvent(newUnresolvedGroup, ServerStoredGroupEvent.GROUP_CREATED_EVENT);
        return newUnresolvedGroup;
    }

    /**
     * Creates the specified group on the server stored contact list.
     *
     * @param groupName a String containing the name of the new group.
     * @throws OperationFailedException with code CONTACT_GROUP_ALREADY_EXISTS if the group we're trying
     * to create is already in our contact list.
     */
    public void createGroup(String groupName)
            throws OperationFailedException
    {
        Timber.d("Creating group: %s", groupName);
        ContactGroupJabberImpl existingGroup = findContactGroup(groupName);

        if (existingGroup != null && existingGroup.isPersistent()) {
            Timber.d("ContactGroup %s already exists.", groupName);
            throw new OperationFailedException("ContactGroup " + groupName + " already exists.",
                    OperationFailedException.CONTACT_GROUP_ALREADY_EXISTS);
        }

        RosterGroup newRosterGroup = mRoster.createGroup(groupName);
        ContactGroupJabberImpl newGroup = new ContactGroupJabberImpl(newRosterGroup,
                Collections.emptyIterator(), this, true);
        rootGroup.addSubGroup(newGroup);

        fireGroupEvent(newGroup, ServerStoredGroupEvent.GROUP_CREATED_EVENT);
        Timber.log(TimberLog.FINER, "Group %s created.", groupName);
    }

    /**
     * Removes the specified group from the buddy list.
     *
     * @param groupToRemove the group that we'd like removed.
     */
    public void removeGroup(ContactGroupJabberImpl groupToRemove)
            throws OperationFailedException
    {
        try {
            // first copy the item that will be removed when iterating over group contacts and
            // removing them concurrent exception occurs
            Vector<Contact> localCopy = new Vector<>();
            Iterator<Contact> iter = groupToRemove.contacts();

            while (iter.hasNext()) {
                localCopy.add(iter.next());
            }

            iter = localCopy.iterator();
            while (iter.hasNext()) {
                ContactJabberImpl item = (ContactJabberImpl) iter.next();
                if (item.isPersistent())
                    mRoster.removeEntry(item.getSourceEntry());
            }
        } catch (XMPPException ex) {
            Timber.e(ex, "Error removing group");
            throw new OperationFailedException(ex.getMessage(), OperationFailedException.GENERAL_ERROR, ex);
        } catch (NotLoggedInException | NoResponseException | NotConnectedException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * Removes a contact from the server side list Event will come for successful operation
     *
     * @param contactToRemove ContactJabberImpl
     */
    void removeContact(ContactJabberImpl contactToRemove)
            throws OperationFailedException
    {
        // Allow direct removal of any VolatileContactJabberImpl if it is not DomainBareJid
        if (contactToRemove.getJid() instanceof DomainBareJid)
            return;

        // aTalk implementation is ContactGroup.VOLATILE_GROUP is equivalent to "VolatileContactJabberImpl"
        if ((contactToRemove instanceof VolatileContactJabberImpl) || ((contactToRemove.getParentContactGroup() != null)
                && ContactGroup.VOLATILE_GROUP.equals(contactToRemove.getParentContactGroup().getGroupName()))) {
            contactDeleted(contactToRemove);
            return;
        }

        //don't try to remove non-existing contacts.
        RosterEntry entry = contactToRemove.getSourceEntry();
        if (entry != null) {
            try {
                mRoster.removeEntry(entry);
            } catch (XMPPErrorException ex) {
                String errTxt = "Error removing contact";
                int errorCode = OperationFailedException.INTERNAL_ERROR;
                StanzaError error = ex.getStanzaError();
                if (error != null) {
                    errTxt = error.getDescriptiveText();
                    if (error.getCondition().equals(Condition.internal_server_error))
                        errorCode = OperationFailedException.INTERNAL_SERVER_ERROR;
                    else if (error.getCondition().equals(Condition.forbidden))
                        errorCode = OperationFailedException.FORBIDDEN;
                    else if (error.getCondition().equals(Condition.bad_request))
                        errorCode = OperationFailedException.ILLEGAL_ARGUMENT;
                }
                Timber.e(ex, "%s", errTxt);
                throw new OperationFailedException(errTxt, errorCode, ex);
            } catch (NotLoggedInException | NoResponseException | NotConnectedException | InterruptedException ex) {
                ex.printStackTrace();
            }
        }
    }

    /**
     * Renames the specified group according to the specified new name.
     *
     * @param groupToRename the group that we'd like removed.
     * @param newName the new name of the group
     */
    public void renameGroup(ContactGroupJabberImpl groupToRename, String newName)
    {
        try {
            groupToRename.getSourceGroup().setName(newName);
            groupToRename.setNameCopy(newName);
        } catch (NotConnectedException | NoResponseException | XMPPErrorException | InterruptedException e) {
            Timber.e("Could not rename %s to %s", groupToRename, newName);
        }
        groupToRename.setNameCopy(newName);
    }

    /**
     * Moves the specified <code>contact</code> to the group indicated by <code>newParent</code>.
     *
     * @param contact the contact that we'd like moved under the new group.
     * @param newParent the group where we'd like the parent placed.
     */
    public void moveContact(ContactJabberImpl contact, AbstractContactGroupJabberImpl newParent)
            throws OperationFailedException
    {
        // when the contact is not persistent, coming from NotInContactList group, we need just
        // to add it to the list
        if (!contact.isPersistent()) {
            String contactAddress;
            if (contact instanceof VolatileContactJabberImpl
                    && ((VolatileContactJabberImpl) contact).isPrivateMessagingContact()) {
                contactAddress = contact.getPersistableAddress();
            }
            else {
                contactAddress = contact.getAddress();
            }
            try {
                addContact(newParent, contactAddress);
                return;
            } catch (OperationFailedException ex) {
                Timber.e(ex, "Cannot move contact!");
                throw new OperationFailedException(ex.getMessage(), OperationFailedException.GENERAL_ERROR, ex);
            }
        }

        // create the entry with the new group so it can be removed from other groups if any.
        // modify our reply timeout because some XMPP may send "result" IQ late (> 5 seconds).
        xmppConnection.setReplyTimeout(ProtocolProviderServiceJabberImpl.SMACK_REPLY_EXTENDED_TIMEOUT_30);

        try {
            mRoster.createItemAndRequestSubscription(contact.getSourceEntry().getJid(), contact.getDisplayName(),
                    new String[]{newParent.getGroupName()});
            newParent.addContact(contact);
        } catch (XMPPException ex) {
            Timber.e(ex, "Cannot move contact!");
            throw new OperationFailedException(ex.getMessage(), OperationFailedException.GENERAL_ERROR, ex);
        } catch (NotLoggedInException | NoResponseException | NotConnectedException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            // Reset to default
            xmppConnection.setReplyTimeout(ProtocolProviderServiceJabberImpl.SMACK_REPLY_TIMEOUT_DEFAULT);
        }
    }

    /**
     * Sets a reference to the currently active and valid instance of roster that this list is to
     * be used for retrieving server stored information
     */
    void init(ContactChangesListener presenceChangeListener)
    {
        // FFR: v2.1.6 Huawei nova 3i/Y9 prime (HWINE) android-9, xmppConnection == null
        // This may be called when PPS is not-registered ???? called at RegistrationState.REGISTERED state
        xmppConnection = mPPS.getConnection();
        mRoster = Roster.getInstanceFor(xmppConnection);

        initRoster();
        AvatarManager avatarManager = AvatarManager.getInstanceFor(xmppConnection);
        try {
            avatarManager.saveAccountRoster(xmppConnection.getUser().asBareJid());
        } catch (XmppStringprepException e) {
            e.printStackTrace();
        }

        // roster has been requested or loaded and dispatched, mark this
        synchronized (rosterInitLock) {
            this.isRosterInitialized = true;
        }

        // now send initial presence status that was on hold earlier
        sendInitialStatus();
        // FFR: v3.0.5 Huawei HWELE android-10, presenceChangeListener == null ???
        // presenceChangeListener was init in registering and this method is called at registered????
        presenceChangeListener.processStoredEvents();

        rosterChangeListener = new ChangeListener();
        // v2.2.2. mRoster => NPE
        mRoster.addRosterListener(rosterChangeListener);
    }

    /**
     * Sends the initial presence to server. RFC 6121 says: a client SHOULD request the roster
     * before sending initial presence We extend this and send it after we have dispatched the roster.
     *
     * Note: Ensure only <x xmlns='vcard-temp:x:update'/> extension without the <photo/> element is
     * added if avatar photo has yet to be downloaded from server. Refer to XEP-0153: vCard-Based
     * Avatars section Example 6. User Is Not Ready to Advertise an Image
     */
    private void sendInitialStatus()
    {
        // if we have initial status saved then send it after roster has completed
        if (initialStatus != null) {
            try {
                parentOperationSet.publishPresenceStatus(initialStatus, initialStatusMessage);
            } catch (Exception ex) {
                Timber.e(ex, "Error publishing initial presence");
            }
        }
        // Send <presence/> only we do not have OperationSetPersistentPresence feature, which are
        // more readily to support <Presence/> sending with <photo/> tag
        else if (mPPS.getOperationSet(OperationSetPersistentPresence.class) == null) {
            Timber.w("Smack sending presence without OpSetPP support!");
            try {
                XMPPConnection connection = mPPS.getConnection();
                PresenceBuilder presenceBuilder = connection.getStanzaFactory().buildPresenceStanza()
                        .ofType(Presence.Type.available);
                connection.sendStanza(presenceBuilder.build());
            } catch (NotConnectedException | InterruptedException e) {
                e.printStackTrace();
            }
        }
        // clean
        initialStatus = null;
        initialStatusMessage = null;
    }

    /**
     * Cleanups references and listeners.
     */
    void cleanup()
    {
        if (imageRetriever != null) {
            imageRetriever.quit();
            imageRetriever = null;
        }

        if (mRoster != null)
            mRoster.removeRosterListener(rosterChangeListener);

        this.rosterChangeListener = null;
        mRoster = null;

        synchronized (rosterInitLock) {
            this.isRosterInitialized = false;
        }
    }

    /**
     * When the protocol is online this method is used to Synchronous the current metaContactList
     * with the received roster
     */
    private synchronized void initRoster()
    {
        // first if non-filed entries will move them in a group
        if (mRoster.getUnfiledEntryCount() > 0) {
            for (RosterEntry item : mRoster.getUnfiledEntries()) {
                ContactJabberImpl contact = findContactById(item.getJid());

                // some services automatically add contacts from their address book to the roster,
                // and those contacts are with subscription none. If such already exist, remove them.
                // This is typically our own contact
                if (!isEntryDisplayable(item)) {
                    if (contact != null) {
                        ContactGroup parent = contact.getParentContactGroup();
                        if (parent instanceof RootContactGroupJabberImpl)
                            ((RootContactGroupJabberImpl) parent).removeContact(contact);
                        else
                            ((ContactGroupJabberImpl) parent).removeContact(contact);
                        fireContactRemoved(parent, contact);
                    }
                    continue;
                }

                // if there is no such contact create it
                if (contact == null) {
                    contact = new ContactJabberImpl(item, this, true, true);
                    rootGroup.addContact(contact);
                    fireContactAdded(rootGroup, contact);
                }
                else {
                    ContactGroup group = contact.getParentContactGroup();
                    // cmeng - Cannot just compare groups of different instanceOf.
                    // Do not move contact or if request is of the same group.
                    if (!rootGroup.getGroupName().equals(group.getGroupName())) {
                        contactMoved(group, rootGroup, contact);
                    }
                    // if contact exist so resolve it
                    contact.setResolved(item);

                    // fire an event saying that the non-filed contact has been resolved
                    fireContactResolved(rootGroup, contact);
                }

                /*
                 * process status if any that was received while the roster reply packet was received and added
                 * our presence listener. Fixes a problem where Presence packets can be received before the roster
                 * items packet, and we miss it, cause we add our listener after roster is received and smack
                 * don't allow to add our listener earlier
                 */
                // cmeng - already done in either fireContactAdded() or fireContactResolved() method
                // Duplicated entry in storedPresences if allow to be executed.
                // parentOperationSet.firePresenceStatusChanged(mRoster.getPresence(item.getJid()));
            }
        }

        // now search all root contacts for unresolved ones
        Iterator<Contact> contacts = rootGroup.contacts();
        List<ContactJabberImpl> contactsToRemove = new ArrayList<>();
        while (contacts.hasNext()) {
            ContactJabberImpl contact = (ContactJabberImpl) contacts.next();
            if (!contact.isResolved()) {
                contactsToRemove.add(contact);
            }
        }

        for (ContactJabberImpl contact : contactsToRemove) {
            rootGroup.removeContact(contact);
            fireContactRemoved(rootGroup, contact);
        }
        contactsToRemove.clear();

        for (RosterGroup item : mRoster.getGroups()) {
            ContactGroupJabberImpl group = findContactGroup(item.getName());
            if (group != null) {
                // the group exist so just resolved. The group will check and create or resolve its entries
                group.setResolved(item);

                // fire an event saying that the group has been resolved
                fireGroupEvent(group, ServerStoredGroupEvent.GROUP_RESOLVED_EVENT);
            }
        }

        Iterator<ContactGroup> iterGroups = rootGroup.subgroups();
        List<ContactGroupJabberImpl> groupsToRemove = new ArrayList<>();
        while (iterGroups.hasNext()) {
            // group can be RootContactGroupJabberImpl or ContactGroupJabberImpl
            ContactGroupJabberImpl group = (ContactGroupJabberImpl) iterGroups.next();

            // cmeng: all current aTalk groups are set to be persistent including volatileGroup (domainJid);
            // Need to skip further checking to avoid removal on isResolved == false
            // so invalid to skip non persistent groups if (!group.isPersistent())
            if (ContactGroup.VOLATILE_GROUP.equals(group.getGroupName()))
                continue;

            // cmeng - Must not remove root group in new SQLite database implementation.
            // Need special check here as all ContactGroups have been casted to
            // ContactGroupJabberImpl including RootContactGroupJabberImpl
            if (!ContactGroup.ROOT_PROTO_GROUP_UID.equals(group.getGroupName()) && !group.isResolved()) {
                groupsToRemove.add(group);
            }

            Iterator<Contact> iterContacts = group.contacts();
            while (iterContacts.hasNext()) {
                ContactJabberImpl contact = (ContactJabberImpl) iterContacts.next();
                if (!contact.isResolved()) {
                    contactsToRemove.add(contact);
                }
            }
            for (ContactJabberImpl contact : contactsToRemove) {
                group.removeContact(contact);
                fireContactRemoved(group, contact);
            }
            contactsToRemove.clear();
        }

        for (ContactGroupJabberImpl group : groupsToRemove) {
            rootGroup.removeSubGroup(group);
            fireGroupEvent(group, ServerStoredGroupEvent.GROUP_REMOVED_EVENT);
        }

        // fill in root group
        for (RosterGroup item : mRoster.getGroups()) {
            ContactGroupJabberImpl group = findContactGroup(item.getName());

            // create the group as it doesn't exist
            if (group == null) {
                ContactGroupJabberImpl newGroup
                        = new ContactGroupJabberImpl(item, item.getEntries().iterator(), this, true);
                rootGroup.addSubGroup(newGroup);

                // tell listeners about the added group
                fireGroupEvent(newGroup, ServerStoredGroupEvent.GROUP_CREATED_EVENT);

                // if presence was already received it, we must check & dispatch it
                if (mRoster != null) {
                    Iterator<Contact> cIter = newGroup.contacts();
                    while (cIter.hasNext()) {
                        Jid contactJid = cIter.next().getJid();
                        parentOperationSet.firePresenceStatusChanged(mRoster.getPresence(contactJid.asBareJid()));
                    }
                }
            }
        }
    }

    /**
     * Returns the volatile group that we use when creating volatile contacts.
     * VolatileGroup is now set to persistent to save to DB
     *
     * @return ContactGroupJabberImpl
     */
    ContactGroupJabberImpl getNonPersistentGroup()
    {
        for (int i = 0; i < getRootGroup().countSubgroups(); i++) {
            ContactGroupJabberImpl gr = (ContactGroupJabberImpl) getRootGroup().getGroup(i);
            if (ContactGroup.VOLATILE_GROUP.equals(gr.getGroupName()))
                return gr;
        }
        return null;
    }

    /**
     * Make the parent persistent presence operation set dispatch a contact added event.
     *
     * @param parentGroup the group where the new contact was added
     * @param contact the contact that was added
     */
    public void fireContactAdded(ContactGroup parentGroup, ContactJabberImpl contact)
    {
        // bail out if no one's listening
        if (parentOperationSet == null) {
            Timber.d("No presence op. set available. Bailing out.");
            return;
        }

        // if we are already registered (roster != null), and we are currently creating the contact list,
        // presences maybe already received before we have created the contacts, so let's check
        if (mRoster != null) {
            parentOperationSet.firePresenceStatusChanged(mRoster.getPresence(contact.getJid().asBareJid()));
        }

        // dispatch
        parentOperationSet.fireSubscriptionEvent(contact, parentGroup, SubscriptionEvent.SUBSCRIPTION_CREATED);
    }

    /**
     * Make the parent persistent presence operation set dispatch a contact resolved event.
     *
     * @param parentGroup the group the resolved contact belongs to.
     * @param contact the contact that was resolved
     */
    public void fireContactResolved(ContactGroup parentGroup, ContactJabberImpl contact)
    {
        // bail out if no one's listening
        if (parentOperationSet == null) {
            Timber.d("No presence op. set available. Bailing out.");
            return;
        }

        // if we are already registered(roster != null) and we are currently creating the contact
        // list, presences maybe already received before we have created the contacts, so lets check
        if (mRoster != null) {
            parentOperationSet.firePresenceStatusChanged(mRoster.getPresence(contact.getJid().asBareJid()));
        }

        // dispatch
        parentOperationSet.fireSubscriptionEvent(contact, parentGroup, SubscriptionEvent.SUBSCRIPTION_RESOLVED);
    }

    /**
     * When there is no photo image for a contact, we need to retrieve it by adding the contact
     * into contactsForUpdate arrayList for image update
     *
     * @param contact ContactJabberImpl
     * @see ImageRetriever#contactsForUpdate
     */
    protected void addContactForImageUpdate(ContactJabberImpl contact)
    {
        if (contact instanceof VolatileContactJabberImpl
                && ((VolatileContactJabberImpl) contact).isPrivateMessagingContact())
            return;

        if (imageRetriever == null) {
            imageRetriever = new ImageRetriever();
            imageRetriever.start();
        }
        imageRetriever.addContact(contact);
    }

    /**
     * @param enable if set enable the retrieval of avatar from server if null
     */
    public void setRetrieveOnStart(boolean enable)
    {
        infoRetrieveOnStart = enable;
    }

    /**
     * Some roster entries are not supposed to be seen. Like some services automatically add
     * contacts from their address book to the roster and those contacts are with subscription none.
     * Best practices in XEP-0162.
     * - subscription='both' or subscription='to'
     * - ((subscription='none' or subscription='from') and ask='subscribe')
     * - ((subscription='none' or subscription='from') and (name attribute or group child))
     *
     * @param entry the entry to check.
     * @return is item to be hidden/ignored.
     */
    static boolean isEntryDisplayable(RosterEntry entry)
    {
        if (entry.getType() == both || entry.getType() == to) {
            return true;
        }
        // cmeng = entry in rootGroup does not have group attribute ???
        else if ((entry.getType() == none || entry.getType() == from)
                && (entry.isSubscriptionPending() || entry.getGroups().size() > 0)) {
            return true;
        }
        return false;
    }

    /**
     * Removes contact from client side.
     *
     * @param contact the contact to be deleted.
     */
    private void contactDeleted(ContactJabberImpl contact)
    {
        ContactGroup group = findContactGroup(contact);
        if (group == null) {
            Timber.log(TimberLog.FINER, "Could not find ParentGroup for deleted entry:%s", contact.getAddress());
            return;
        }

        if (group instanceof ContactGroupJabberImpl) {
            ContactGroupJabberImpl groupImpl = (ContactGroupJabberImpl) group;

            // remove the contact from parent group
            groupImpl.removeContact(contact);

            // Remove groupImpl from rootGroup if it is empty list and it is not the rootGroup.
            // This deleted group will also be removed from server if empty
            if ((groupImpl.countContacts() == 0) && (groupImpl != getRootGroup())) {
                rootGroup.removeSubGroup(groupImpl);

                fireContactRemoved(groupImpl, contact);
                fireGroupEvent(groupImpl, ServerStoredGroupEvent.GROUP_REMOVED_EVENT);
            }
            else
                fireContactRemoved(groupImpl, contact);
        }
        else if (group instanceof RootContactGroupJabberImpl) {
            rootGroup.removeContact(contact);
            fireContactRemoved(rootGroup, contact);
        }
    }

    /**
     * Receive changes in the roster.
     */
    private class ChangeListener implements RosterListener
    {
        /**
         * Received an event when entry is added to the server stored list
         *
         * @param addresses Collection of contact Jid
         */
        public void entriesAdded(Collection<Jid> addresses)
        {
            Timber.log(TimberLog.FINER, "entries Added %s", addresses);

            for (Jid id : addresses) {
                addEntryToContactList(id);
            }
        }

        /**
         * Adds the entry to our local contactList. If contact exists and is persistent but not
         * resolved, we resolve it and return it without adding new contact. If the contact exists
         * and is not persistent, we remove it, to avoid duplicate contacts and add the new one.
         * All entries must be displayable before we done anything with them.
         *
         * @param rosterEntryID the entry id.
         * @return the newly created contact.
         */
        private ContactJabberImpl addEntryToContactList(Jid rosterEntryID)
        {
            RosterEntry entry = mRoster.getEntry(rosterEntryID.asBareJid());
            if (!isEntryDisplayable(entry))
                return null;

            ContactJabberImpl contact = findContactById(entry.getJid());
            if (contact == null) {
                contact = findPrivateContactByRealId(entry.getJid());
            }

            if (contact != null) {
                if (contact.isPersistent()) {
                    contact.setResolved(entry);
                    return contact;
                }
                else if (contact instanceof VolatileContactJabberImpl) {
                    ContactGroup oldParentGroup = contact.getParentContactGroup();
                    // If contact is in 'notInContactList' we must remove it from there in order
                    // to correctly process adding contact this happens if we accept subscribe
                    // request not from sip-communicator
                    if (oldParentGroup instanceof ContactGroupJabberImpl && !oldParentGroup.isPersistent()) {
                        ((ContactGroupJabberImpl) oldParentGroup).removeContact(contact);
                        fireContactRemoved(oldParentGroup, contact);
                    }
                }
                else
                    return contact;
            }

            // Not in local group, then create and add new local contact
            contact = new ContactJabberImpl(entry, ServerStoredContactListJabberImpl.this, true, true);
            if (entry.getGroups().size() == 0) {
                // no parent group so its in the root group
                rootGroup.addContact(contact);
                fireContactAdded(rootGroup, contact);
                return contact;
            }

            for (RosterGroup group : entry.getGroups()) {
                ContactGroupJabberImpl parentGroup = findContactGroup(group.getName());

                if (parentGroup != null) {
                    parentGroup.addContact(contact);
                    fireContactAdded(findContactGroup(contact), contact);
                }
                else {
                    // create the group as it doesn't exist
                    ContactGroupJabberImpl newGroup = new ContactGroupJabberImpl(group, group.getEntries().iterator(),
                            ServerStoredContactListJabberImpl.this, true);
                    rootGroup.addSubGroup(newGroup);

                    // tell listeners about the added group
                    fireGroupEvent(newGroup, ServerStoredGroupEvent.GROUP_CREATED_EVENT);
                }
                // as for now we only support contact in one group
                return contact;
            }
            return contact;
        }

        /**
         * Finds private messaging contact by its jabber id.
         *
         * @param id the jabber id.
         * @return the contact or null if the contact is not found.
         */
        private ContactJabberImpl findPrivateContactByRealId(Jid id)
        {
            ContactGroupJabberImpl volatileGroup = getNonPersistentGroup();
            if (volatileGroup == null)
                return null;
            Iterator<Contact> it = volatileGroup.contacts();
            while (it.hasNext()) {
                Contact contact = it.next();
                if (contact.getPersistableAddress() == null)
                    continue;
                if (contact.getPersistableAddress().equals(id.asBareJid().toString())) {
                    return (ContactJabberImpl) contact;
                }
            }
            return null;
        }

        /**
         * Event when an entry is updated. Something new for the entry data
         * or have been added to a new group or removed from one
         *
         * @param addresses Collection of Contact Jid's
         */
        public void entriesUpdated(Collection<Jid> addresses)
        {
            Timber.log(TimberLog.FINER, "entries Updated %s", addresses);

            // will search for group renamed
            for (Jid contactJid : addresses) {
                RosterEntry entry = mRoster.getEntry(contactJid.asBareJid());
                ContactJabberImpl contact = addEntryToContactList(contactJid);

                if (contact == null)
                    continue;

                if (entry.getGroups().isEmpty()) {
                    // check for change in display name
                    checkForRename(entry.getName(), contact);
                    ContactGroup contactGroup = contact.getParentContactGroup();
                    if (!rootGroup.equals(contactGroup)) {
                        contactMoved(contactGroup, rootGroup, contact);
                    }
                }

                for (RosterGroup gr : entry.getGroups()) {
                    ContactGroup cgr = findContactGroup(gr.getName());

                    // Check for ROOT_GROUP_NAME if null
                    if (cgr == null) {
                         // ROOT_GROUP_NAME (contacts) is not listed in subGroups() for search. Need special handle
                        if (ContactGroup.ROOT_GROUP_NAME.equals(gr.getName())) {
                            cgr =  getRootGroup();
                        }
                        // Group does not exist. so it must be a renamed group
                        else {
                            ContactGroupJabberImpl group = findContactGroupByNameCopy(gr.getName());
                            if (group != null) {
                                // just change the source entry
                                group.setSourceGroup(gr);
                                fireGroupEvent(group, ServerStoredGroupEvent.GROUP_RENAMED_EVENT);
                            }
                            else {
                                // the group was renamed in a different location, so we do not have it at
                                // our side now; let's find the group for the contact and rename it;
                                // if it is the only contact in the group then rename, otherwise move
                                ContactGroup currentParentGroup = contact.getParentContactGroup();
                                if (currentParentGroup.countContacts() > 1) {
                                    cgr = currentParentGroup;
                                }
                                else {
                                    // make sure this group name is not present in entry groups
                                    boolean present = false;
                                    for (RosterGroup entryGr : entry.getGroups()) {
                                        if (entryGr.getName().equals(currentParentGroup.getGroupName())) {
                                            present = true;
                                            break;
                                        }
                                    }

                                    if (!present && currentParentGroup instanceof ContactGroupJabberImpl) {
                                        ContactGroupJabberImpl currentGroup = (ContactGroupJabberImpl) currentParentGroup;
                                        currentGroup.setSourceGroup(gr);
                                        fireGroupEvent(currentGroup, ServerStoredGroupEvent.GROUP_RENAMED_EVENT);
                                    }
                                }
                            }
                        }
                    }

                    // the group is found the contact may be moved from one group to another
                    if (cgr != null) {
                        ContactGroup contactGroup = contact.getParentContactGroup();

                        // contact parent group is different, then add it to the new one
                        if (!gr.getName().equals(contactGroup.getGroupName())) {
                            ContactGroup newParentGroup = findContactGroup(gr.getName());

                            // Proceed to add the new parent group if none found
                            if (newParentGroup == null) {
                                // ROOT_GROUP_NAME (contacts) is not listed in subGroups() for search. Need special handle
                                if (ContactGroup.ROOT_GROUP_NAME.equals(gr.getName())) {
                                    newParentGroup = getRootGroup();
                                }
                                else {
                                    // create the group as it doesn't exist
                                    newParentGroup = new ContactGroupJabberImpl(gr, Collections.emptyIterator(),
                                            ServerStoredContactListJabberImpl.this, true);

                                rootGroup.addSubGroup(newParentGroup);
                                // tell listeners about the added group
                                fireGroupEvent(newParentGroup, ServerStoredGroupEvent.GROUP_CREATED_EVENT);
                                }
                            }
                            contactMoved(contactGroup, newParentGroup, contact);
                        }
                        else {
                            // check for change in display name
                            checkForRename(entry.getName(), contact);
                        }
                    }
                }
            }
        }

        /**
         * Checks the entry and the contact whether the display name has changed.
         *
         * @param newValue new display name value
         * @param contact the contact to check
         */
        private void checkForRename(String newValue, ContactJabberImpl contact)
        {
            // check for change in display name
            if (newValue != null && !newValue.equals(contact.getServerDisplayName())) {
                String oldValue = contact.getServerDisplayName();
                contact.setServerDisplayName(newValue);
                parentOperationSet.fireContactPropertyChangeEvent(contact,
                        ContactPropertyChangeEvent.PROPERTY_DISPLAY_NAME, oldValue, newValue);
            }
        }

        /**
         * Event received when entry has been removed from the list
         *
         * @param addresses Collection
         */
        @Override
        public void entriesDeleted(Collection<Jid> addresses)
        {
            for (Jid contactJid : addresses) {
                Timber.log(TimberLog.FINER, "entry deleted %s", contactJid);

                ContactJabberImpl contact = findContactById(contactJid);
                if (contact == null) {
                    Timber.d("Could not find contact for deleted entry: %s", contactJid);
                    continue;
                }
                contactDeleted(contact);
            }
        }

        /**
         * Not used here.
         *
         * @param presence the presence that changed.
         */
        public void presenceChanged(Presence presence)
        {
        }
    }

    /**
     * Thread for retrieving contacts' images.
     */
    private class ImageRetriever extends Thread
    {
        /**
         * list with the accounts with missing image
         */
        private final List<ContactJabberImpl> contactsForUpdate = new Vector<>();

        /**
         * Should we stop.
         */
        private boolean running = false;

        /**
         * Creates image retrieving.
         */
        ImageRetriever()
        {
            setDaemon(true);
        }

        /**
         * Thread entry point.
         */
        @Override
        public void run()
        {
            try {
                Collection<ContactJabberImpl> copyContactsForUpdate;
                running = true;
                while (running) {
                    synchronized (contactsForUpdate) {
                        if (contactsForUpdate.isEmpty())
                            contactsForUpdate.wait();

                        if (!running)
                            return;

                        copyContactsForUpdate = new Vector<>(contactsForUpdate);
                        contactsForUpdate.clear();
                    }

                    for (ContactJabberImpl contact : copyContactsForUpdate) {
                        byte[] imgBytes = getAvatar(contact);
                        if (imgBytes != null) {
                            byte[] oldImage = contact.getImage(false);

                            contact.setImage(imgBytes);
                            parentOperationSet.fireContactPropertyChangeEvent(contact,
                                    ContactPropertyChangeEvent.PROPERTY_IMAGE, oldImage, imgBytes);
                        }
                        else
                            // set an empty image data so it would not be queried again
                            contact.setImage(new byte[0]);
                    }
                }
            } catch (InterruptedException ex) {
                Timber.e(ex, "ImageRetriever error waiting will stop now!");
            }
        }

        /**
         * Add contact for retrieving:
         * - if the provider is registered notify the retriever to get the nicks
         * - if we are not registered add a listener to wait for registering
         *
         * @param contact ContactJabberImpl
         */
        void addContact(ContactJabberImpl contact)
        {
            synchronized (contactsForUpdate) {
                if (!contactsForUpdate.contains(contact)) {
                    contactsForUpdate.add(contact);
                    contactsForUpdate.notifyAll();
                }
            }
        }

        /**
         * Stops this thread.
         */
        void quit()
        {
            synchronized (contactsForUpdate) {
                running = false;
                contactsForUpdate.notifyAll();
            }
        }

        /**
         * Retrieves the avatar. Use image from persistent storage if found. Otherwise proceed to
         * load avatar from VCard.
         *
         * @param contact the contact.
         * @return the contact avatar.
         */
        private byte[] getAvatar(ContactJabberImpl contact)
        {
            BareJid userJid = contact.getJid().asBareJid();
            String userId = userJid.toString();
            byte[] result = VCardAvatarManager.getAvatarImageByJid(userJid);

            if ((result == null) && infoRetrieveOnStart) {
                Timber.i("Proceed to getAvatar for: %s", userId);
                try {
                    Iterator<ServerStoredDetails.GenericDetail> iter
                            = infoRetriever.getDetails(userJid, ServerStoredDetails.ImageDetail.class);

                    if (iter.hasNext()) {
                        ServerStoredDetails.ImageDetail imgDetail = (ServerStoredDetails.ImageDetail) iter.next();
                        result = imgDetail.getBytes();
                    }
                } catch (Exception ex) {
                    Timber.d(ex, "Cannot load image for contact %s: %s", contact, ex.getMessage());
                }
                if (result == null) {
                    result = searchForCustomAvatar(userId);
                }
            }
            if (result == null) // return no photo if null
                result = new byte[0];

            return result;
        }
    }

    /**
     * Query custom avatar services and returns the first found avatar.
     *
     * @return the found avatar if any.
     */
    private byte[] searchForCustomAvatar(String address)
    {
        try {
            ServiceReference[] refs
                    = JabberActivator.bundleContext.getServiceReferences(CustomAvatarService.class.getName(), null);

            if (refs == null)
                return null;

            for (ServiceReference r : refs) {
                CustomAvatarService avatarService = (CustomAvatarService) JabberActivator.bundleContext.getService(r);

                byte[] res = avatarService.getAvatar(address);
                if (res != null)
                    return res;
            }
        } catch (Throwable t) {
            // if something is wrong just return empty image
        }
        return null;
    }

    /**
     * Handles moving of contact from one group to another.
     *
     * @param oldGroup old group of the contact.
     * @param newGroup new group of the contact.
     * @param contact contact to move
     */
    private void contactMoved(ContactGroup oldGroup, ContactGroup newGroup,
            ContactJabberImpl contact)
    {
        // The contact is moved to another group first, before removing it from the original one
        if (oldGroup instanceof ContactGroupJabberImpl)
            ((ContactGroupJabberImpl) oldGroup).removeContact(contact);
        else if (oldGroup instanceof RootContactGroupJabberImpl)
            ((RootContactGroupJabberImpl) oldGroup).removeContact(contact);

        if (newGroup instanceof ContactGroupJabberImpl)
            ((ContactGroupJabberImpl) newGroup).addContact(contact);
        else if (newGroup instanceof RootContactGroupJabberImpl)
            ((RootContactGroupJabberImpl) newGroup).addContact(contact);

        fireContactMoved(oldGroup, newGroup, contact);

        if (oldGroup instanceof ContactGroupJabberImpl && oldGroup.countContacts() == 0) {
            // in xmpp if group is empty it is removed
            rootGroup.removeSubGroup((ContactGroupJabberImpl) oldGroup);
            fireGroupEvent((ContactGroupJabberImpl) oldGroup, ServerStoredGroupEvent.GROUP_REMOVED_EVENT);
        }
    }

    /**
     * Completes the identifier with the server part if no server part was previously added.
     *
     * @param id the initial identifier as added by the user
     */
    private EntityBareJid parseAddressString(String id)
            throws OperationFailedException
    {
        try {
            Jid temp = JidCreate.from(id);
            if (!temp.hasLocalpart()) {
                AccountID accountID = mPPS.getAccountID();
                Jid accountJid = JidCreate.from(accountID.getUserID());
                return JidCreate.entityBareFrom(Localpart.from(id), accountJid.getDomain());
            }
            return temp.asEntityBareJidOrThrow();
        } catch (XmppStringprepException | IllegalArgumentException e) {
            throw new OperationFailedException("Could not parse: " + id, 0, e);
        }
    }

    /**
     * Return all the presences for the user or an EMPTY_LIST.
     *
     * @param userJid the bareJid of the user to check for presences.
     * @return all the presences available for the user or an EMPTY_LIST.
     */
    public List<Presence> getPresences(BareJid userJid)
    {
        return ((mRoster == null) || (userJid == null)) ? Collections.EMPTY_LIST : mRoster.getPresences(userJid);
    }

    /**
     * Returns whether roster is initialized.
     *
     * @return whether roster is initialized.
     */
    public boolean isRosterInitialized()
    {
        return isRosterInitialized;
    }

    /**
     * The lock around isRosterInitialized variable.
     *
     * @return the lock around isRosterInitialized variable.
     */
    Object getRosterInitLock()
    {
        return rosterInitLock;
    }

    /**
     * Saves the initial status for later dispatching.
     *
     * @param initialStatus to be dispatched later.
     */
    void setInitialStatus(PresenceStatus initialStatus)
    {
        this.initialStatus = initialStatus;
    }

    /**
     * Saves the initial status message for later dispatching.
     *
     * @param initialStatusMessage to be dispatched later.
     */
    void setInitialStatusMessage(String initialStatusMessage)
    {
        this.initialStatusMessage = initialStatusMessage;
    }
}
