/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.java.sip.communicator.impl.protocol.jabber;

import java.math.*;
import java.net.*;
import java.security.*;
import java.security.cert.*;
import java.text.*;
import java.util.*;

import javax.net.ssl.*;

import net.java.sip.communicator.impl.protocol.jabber.debugger.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.carbon.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.coin.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.colibri.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.inputevt.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.jibri.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.jingle.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.jingleinfo.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.keepalive.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.messagecorrection.*;
import net.java.sip.communicator.impl.protocol.jabber.extensions.version.*;
import net.java.sip.communicator.service.certificate.*;
import net.java.sip.communicator.service.dns.*;
import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.service.protocol.jabber.*;
import net.java.sip.communicator.service.protocol.jabberconstants.*;
import net.java.sip.communicator.util.*;
import net.java.sip.communicator.util.Logger;

import org.jitsi.service.configuration.*;
import org.jitsi.service.neomedia.*;
import org.jitsi.util.*;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smack.provider.*;
import org.jivesoftware.smack.util.*;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.*;
import org.jivesoftware.smackx.packet.*;
import org.xmlpull.v1.*;
import org.xmpp.jnodes.smack.*;

/**
 * An implementation of the protocol provider service over the Jabber protocol
 *
 * @author Damian Minkov
 * @author Symphorien Wanko
 * @author Lyubomir Marinov
 * @author Yana Stamcheva
 * @author Emil Ivov
 * @author Hristo Terezov
 */
public class ProtocolProviderServiceJabberImpl
    extends AbstractProtocolProviderService
{
    /**
     * Logger of this class
     */
    private static final Logger logger =
        Logger.getLogger(ProtocolProviderServiceJabberImpl.class);

    /**
     * Jingle's Discovery Info common URN.
     */
    public static final String URN_XMPP_JINGLE = JingleIQ.NAMESPACE;

    /**
     * Jingle's Discovery Info URN for RTP support.
     */
    public static final String URN_XMPP_JINGLE_RTP
        = RtpDescriptionPacketExtension.NAMESPACE;

    /**
     * Jingle's Discovery Info URN for RTP support with audio.
     */
    public static final String URN_XMPP_JINGLE_RTP_AUDIO
        = "urn:xmpp:jingle:apps:rtp:audio";

    /**
     * Jingle's Discovery Info URN for RTP support with video.
     */
    public static final String URN_XMPP_JINGLE_RTP_VIDEO
        = "urn:xmpp:jingle:apps:rtp:video";

    /**
     * Jingle's Discovery Info URN for ZRTP support with RTP.
     */
    public static final String URN_XMPP_JINGLE_RTP_ZRTP
        = ZrtpHashPacketExtension.NAMESPACE;

    /**
     * Jingle's Discovery Info URN for ICE_UDP transport support.
     */
    public static final String URN_XMPP_JINGLE_RAW_UDP_0
        = RawUdpTransportPacketExtension.NAMESPACE;

    /**
     * Jingle's Discovery Info URN for ICE_UDP transport support.
     */
    public static final String URN_XMPP_JINGLE_ICE_UDP_1
        = IceUdpTransportPacketExtension.NAMESPACE;

    /**
     * Jingle's Discovery Info URN for Jingle Nodes support.
     */
    public static final String URN_XMPP_JINGLE_NODES
        = "http://jabber.org/protocol/jinglenodes";

    /**
     * Jingle's Discovery Info URN for "XEP-0251: Jingle Session Transfer"
     * support.
     */
    public static final String URN_XMPP_JINGLE_TRANSFER_0
        = TransferPacketExtension.NAMESPACE;

    /**
     * Jingle's Discovery Info URN for "XEP-298 :Delivering Conference
     * Information to Jingle Participants (Coin)" support.
     */
    public static final String URN_XMPP_JINGLE_COIN = "urn:xmpp:coin";

    /**
     * Jingle's Discovery Info URN for &quot;XEP-0320: Use of DTLS-SRTP in
     * Jingle Sessions&quot;.
     */
    public static final String URN_XMPP_JINGLE_DTLS_SRTP
        = "urn:xmpp:jingle:apps:dtls:0";

    /**
     * Discovery Info URN for classic RFC3264-style Offer/Answer negotiation
     * with no support for Trickle ICE and low tolerance to transport/payload
     * separation. Defined in XEP-0176
     */
    public static final String URN_IETF_RFC_3264 = "urn:ietf:rfc:3264";

    /**
     * http://xmpp.org/extensions/xep-0092.html Software Version.
     *
     */
    // Used in JVB
    @SuppressWarnings("unused")
    public static final String URN_XMPP_IQ_VERSION = "jabber:iq:version";

    /**
     * Jingle's Discovery Info URN for "XEP-0294: Jingle RTP Header Extensions
     * Negotiation" support.
     */
    public static final String URN_XMPP_JINGLE_RTP_HDREXT =
        "urn:xmpp:jingle:apps:rtp:rtp-hdrext:0";

    /**
     * URN for XEP-0077 inband registration
     */
    public static final String URN_REGISTER = "jabber:iq:register";

    /**
     * The name of the property under which the user may specify if the desktop
     * streaming or sharing should be disabled.
     */
    private static final String IS_DESKTOP_STREAMING_DISABLED
        = "net.java.sip.communicator.impl.protocol.jabber." +
            "DESKTOP_STREAMING_DISABLED";

    /**
     * The name of the property under which the user may specify if audio/video
     * calls should be disabled.
     */
    private static final String IS_CALLING_DISABLED
        = "net.java.sip.communicator.impl.protocol.jabber.CALLING_DISABLED";

    /**
     * Smack packet reply timeout.
     */
    public static final int SMACK_PACKET_REPLY_TIMEOUT = 45000;

    /**
     * Property for vcard reply timeout. Time to wait before
     * we think vcard retrieving has timeouted, default value
     * of smack is 5000 (5 sec.).
     */
    public static final String VCARD_REPLY_TIMEOUT_PROPERTY =
        "net.java.sip.communicator.impl.protocol.jabber.VCARD_REPLY_TIMEOUT";

    /**
     * XMPP signaling DSCP configuration property name.
     */
    private static final String XMPP_DSCP_PROPERTY =
        "net.java.sip.communicator.impl.protocol.XMPP_DSCP";

    /**
     * Indicates if user search is disabled.
     */
    private static final String IS_USER_SEARCH_ENABLED_PROPERTY
        = "USER_SEARCH_ENABLED";

    /**
     * Google voice domain name.
     */
    public static final String GOOGLE_VOICE_DOMAIN = "voice.google.com";

    /**
     * Used to connect to a XMPP server.
     */
    private Connection connection;

    /**
     * The socket address of the XMPP server.
     */
    private InetSocketAddress address;

    /**
     * Indicates whether or not the provider is initialized and ready for use.
     */
    private boolean isInitialized = false;

    /**
     * We use this to lock access to initialization.
     */
    private final Object initializationLock = new Object();

    /**
     * The identifier of the account that this provider represents.
     */
    private JabberAccountID accountID = null;

    /**
     * Used when we need to re-register
     */
    private SecurityAuthority authority = null;

    /**
     * The resource we will use when connecting during this run.
     */
    private String resource = null;

    /**
     * The icon corresponding to the jabber protocol.
     */
    private ProtocolIconJabberImpl jabberIcon;

    /**
     * A set of features supported by our Jabber implementation.
     * In general, we add new feature(s) when we add new operation sets.
     * (see xep-0030 : http://www.xmpp.org/extensions/xep-0030.html#info).
     * Example : to tell the world that we support jingle, we simply have
     * to do :
     * supportedFeatures.add("http://www.xmpp.org/extensions/xep-0166.html#ns");
     * Beware there is no canonical mapping between op set and jabber features
     * (op set is a SC "concept"). This means that one op set in SC can
     * correspond to many jabber features. It is also possible that there is no
     * jabber feature corresponding to a SC op set or again,
     * we can currently support some features wich do not have a specific
     * op set in SC (the mandatory feature :
     * http://jabber.org/protocol/disco#info is one example).
     * We can find features corresponding to op set in the xep(s) related
     * to implemented functionality.
     */
    private final List<String> supportedFeatures = new ArrayList<String>();

    /**
     * The <tt>ServiceDiscoveryManager</tt> is responsible for advertising
     * <tt>supportedFeatures</tt> when asked by a remote client. It can also
     * be used to query remote clients for supported features.
     */
    private ScServiceDiscoveryManager discoveryManager = null;

    /**
     * The <tt>OperationSetContactCapabilities</tt> of this
     * <tt>ProtocolProviderService</tt> which is the service-public counterpart
     * of {@link #discoveryManager}.
     */
    private OperationSetContactCapabilitiesJabberImpl opsetContactCapabilities;

    /**
     * The statuses.
     */
    private JabberStatusEnum jabberStatusEnum;

    /**
     * The service we use to interact with user.
     */
    private CertificateService guiVerification;

    /**
     * Used with tls connecting when certificates are not trusted
     * and we ask the user to confirm connection. When some timeout expires
     * connect method returns, and we use abortConnecting to abort further
     * execution cause after user chooses we make further processing from there.
     */
    private boolean abortConnecting = false;

    /**
     * Flag indicating are we currently executing connectAndLogin method.
     */
    private boolean inConnectAndLogin = false;

    /**
     * Object used to synchronize the flag inConnectAndLogin.
     */
    private final Object connectAndLoginLock = new Object();

    /**
     * If an event occurs during login we fire it at the end of the login
     * process (at the end of connectAndLogin method).
     */
    private RegistrationStateChangeEvent eventDuringLogin;

    /**
     * Listens for connection closes or errors.
     */
    private JabberConnectionListener connectionListener;

    /**
     * The details of the proxy we are using to connect to the server (if any)
     */
    private org.jivesoftware.smack.proxy.ProxyInfo proxy;

    /**
     * State for connect and login state.
     */
    enum ConnectState
    {
        /**
         * Abort any further connecting.
         */
        ABORT_CONNECTING,
        /**
         * Continue trying with next address.
         */
        CONTINUE_TRYING,
        /**
         * Stop trying we succeeded or just have a final state for
         * the whole connecting procedure.
         */
        STOP_TRYING
    }

    /**
     * The debugger who logs packets.
     */
    private SmackPacketDebugger debugger = null;

    /**
     * Jingle Nodes service.
     */
    private SmackServiceNode jingleNodesServiceNode = null;

    /**
     * Synchronization object to monitore jingle nodes auto discovery.
     */
    private final Object jingleNodesSyncRoot = new Object();

    /**
     * Stores user credentials for local use if user hasn't stored
     * its password.
     */
    private UserCredentials userCredentials = null;

    /**
     * The currently running keepAliveManager if enabled.
     */
    private KeepAliveManager keepAliveManager = null;

    /**
     * The version manager.
     */
    private VersionManager versionManager = null;

    // load xmpp manager classes
    static
    {
        if(OSUtils.IS_ANDROID)
            loadJabberServiceClasses();
    }

    /**
     * An <tt>OperationSet</tt> that allows access to connection information used
     * by the protocol provider.
     */
    private class OperationSetConnectionInfoJabberImpl
       implements OperationSetConnectionInfo
    {
       /**
        * @return The XMPP server address.
        */
        @Override
        public InetSocketAddress getServerAddress()
        {
            return address;
        }
    }

    /**
     * Returns the state of the registration of this protocol provider
     * @return the <tt>RegistrationState</tt> that this provider is
     * currently in or null in case it is in a unknown state.
     */
    public RegistrationState getRegistrationState()
    {
        if(connection == null)
            return RegistrationState.UNREGISTERED;
        else if(connection.isConnected() && connection.isAuthenticated())
            return RegistrationState.REGISTERED;
        else
            return RegistrationState.UNREGISTERED;
    }

    /**
     * Return the certificate verification service impl.
     * @return the CertificateVerification service.
     */
    private CertificateService getCertificateVerificationService()
    {
        if(guiVerification == null)
        {
            guiVerification
                = ServiceUtils.getService(
                        JabberActivator.getBundleContext(),
                        CertificateService.class);
        }

        return guiVerification;
    }

    /**
     * Starts the registration process. Connection details such as
     * registration server, user name/number are provided through the
     * configuration service through implementation specific properties.
     *
     * @param authority the security authority that will be used for resolving
     *        any security challenges that may be returned during the
     *        registration or at any moment while we're registered.
     * @throws OperationFailedException with the corresponding code it the
     * registration fails for some reason (e.g. a networking error or an
     * implementation problem).
     */
    public void register(final SecurityAuthority authority)
        throws OperationFailedException
    {
        if(authority == null)
            throw new IllegalArgumentException(
                "The register method needs a valid non-null authority impl "
                + " in order to be able and retrieve passwords.");

        this.authority = authority;

        try
        {
            // reset states
            abortConnecting = false;

            // indicate we started connectAndLogin process
            synchronized(connectAndLoginLock)
            {
                inConnectAndLogin = true;
            }

            initializeConnectAndLogin(authority,
                SecurityAuthority.AUTHENTICATION_REQUIRED);
        }
        catch (XMPPException ex)
        {
            logger.error("Error registering", ex);

            eventDuringLogin = null;

            fireRegistrationStateChanged(ex);
        }
        finally
        {
            synchronized(connectAndLoginLock)
            {
                // Checks if an error has occurred during login, if so we fire
                // it here in order to avoid a deadlock which occurs in
                // reconnect plugin. The deadlock is cause we fired an event
                // during login process and have locked initializationLock and
                // we cannot unregister from reconnect, cause unregister method
                // also needs this lock.
                if(eventDuringLogin != null)
                {
                    if(eventDuringLogin.getNewState().equals(
                            RegistrationState.CONNECTION_FAILED) ||
                        eventDuringLogin.getNewState().equals(
                            RegistrationState.UNREGISTERED))
                        disconnectAndCleanConnection();

                    fireRegistrationStateChanged(
                        eventDuringLogin.getOldState(),
                        eventDuringLogin.getNewState(),
                        eventDuringLogin.getReasonCode(),
                        eventDuringLogin.getReason());

                    eventDuringLogin = null;
                    inConnectAndLogin = false;
                    return;
                }

                inConnectAndLogin = false;
            }
        }
    }

    /**
     * Connects and logins again to the server.
     *
     * @param authReasonCode indicates the reason of the re-authentication.
     */
    void reregister(int authReasonCode)
    {
        try
        {
            if (logger.isTraceEnabled())
                logger.trace("Trying to reregister us!");

            // sets this if any is trying to use us through registration
            // to know we are not registered
            this.unregisterInternal(false);

            // reset states
            this.abortConnecting = false;

            // indicate we started connectAndLogin process
            synchronized(connectAndLoginLock)
            {
                inConnectAndLogin = true;
            }

            initializeConnectAndLogin(authority, authReasonCode);
        }
        catch(OperationFailedException ex)
        {
            logger.error("Error ReRegistering", ex);

            eventDuringLogin = null;

            disconnectAndCleanConnection();

            fireRegistrationStateChanged(getRegistrationState(),
                RegistrationState.CONNECTION_FAILED,
                RegistrationStateChangeEvent.REASON_INTERNAL_ERROR, null);
        }
        catch (XMPPException ex)
        {
            logger.error("Error ReRegistering", ex);

            eventDuringLogin = null;

            fireRegistrationStateChanged(ex);
        }
        finally
        {
            synchronized(connectAndLoginLock)
            {
                // Checks if an error has occurred during login, if so we fire
                // it here in order to avoid a deadlock which occurs in
                // reconnect plugin. The deadlock is cause we fired an event
                // during login process and have locked initializationLock and
                // we cannot unregister from reconnect, cause unregister method
                // also needs this lock.
                if(eventDuringLogin != null)
                {
                    if(eventDuringLogin.getNewState().equals(
                            RegistrationState.CONNECTION_FAILED) ||
                        eventDuringLogin.getNewState().equals(
                            RegistrationState.UNREGISTERED))
                        disconnectAndCleanConnection();

                    fireRegistrationStateChanged(
                        eventDuringLogin.getOldState(),
                        eventDuringLogin.getNewState(),
                        eventDuringLogin.getReasonCode(),
                        eventDuringLogin.getReason());

                    eventDuringLogin = null;
                    inConnectAndLogin = false;
                    return;
                }

                inConnectAndLogin = false;
            }
        }
    }

    /**
     * Indicates if the XMPP transport channel is using a TLS secured socket.
     *
     * @return True when TLS is used, false otherwise.
     */
    public boolean isSignalingTransportSecure()
    {
        return connection.isSecureConnection();
    }

    /**
     * Returns the "transport" protocol of this instance used to carry the
     * control channel for the current protocol service.
     *
     * @return The "transport" protocol of this instance: TCP, TLS or UNKNOWN.
     */
    public TransportProtocol getTransportProtocol()
    {
        // Without a connection, there is no transport available.
        if(connection != null && connection.isConnected())
        {
            // Transport using a secure connection.
            if(isSignalingTransportSecure())
            {
                return TransportProtocol.TLS;
            }
            // Transport using a unsecure connection.
            return TransportProtocol.TCP;
        }
        return TransportProtocol.UNKNOWN;
    }

    /**
     * Connects and logins to the server
     * @param authority SecurityAuthority
     * @param reasonCode the authentication reason code. Indicates the reason of
     * this authentication.
     * @throws XMPPException if we cannot connect to the server - network problem
     * @throws  OperationFailedException if login parameters
     *          as server port are not correct
     */
    private void initializeConnectAndLogin(SecurityAuthority authority,
                                              int reasonCode)
        throws XMPPException, OperationFailedException
    {
        synchronized(initializationLock)
        {
            // if a thread is waiting for initializationLock and enters
            // lets check whether someone hasn't already tried login and
            // have succeeded,
            // should prevent "Trace possible duplicate connections" prints
            if(isRegistered())
                return;

            JabberLoginStrategy loginStrategy = createLoginStrategy();
            userCredentials = loginStrategy.prepareLogin(authority, reasonCode);
            if(!loginStrategy.loginPreparationSuccessful())
                return;

            String serviceName
                = StringUtils.parseServer(getAccountID().getUserID());

            loadResource();
            loadProxy();
            Roster.setDefaultSubscriptionMode(Roster.SubscriptionMode.manual);

            ConnectState state;
            //[0] = hadDnsSecException
            boolean[] hadDnsSecException = new boolean[]{false};

            // try connecting with auto-detection if enabled
            boolean isServerOverriden =
                getAccountID().getAccountPropertyBoolean(
                    ProtocolProviderFactory.IS_SERVER_OVERRIDDEN, false);

            if(!isServerOverriden)
            {
                state = connectUsingSRVRecords(serviceName,
                        serviceName, hadDnsSecException, loginStrategy);
                if(hadDnsSecException[0])
                {
                    setDnssecLoginFailure();
                    return;
                }
                if(state == ConnectState.ABORT_CONNECTING
                    || state == ConnectState.STOP_TRYING)
                    return;
            }

            // check for custom xmpp domain which we will check for
            // SRV records for server addresses
            String customXMPPDomain = getAccountID()
                .getAccountPropertyString("CUSTOM_XMPP_DOMAIN");

            if(customXMPPDomain != null && !hadDnsSecException[0])
            {
                logger.info("Connect using custom xmpp domain: " +
                    customXMPPDomain);

                state = connectUsingSRVRecords(
                            customXMPPDomain, serviceName,
                            hadDnsSecException, loginStrategy);

                logger.info("state for connectUsingSRVRecords: " + state);

                if(hadDnsSecException[0])
                {
                    setDnssecLoginFailure();
                    return;
                }

                if(state == ConnectState.ABORT_CONNECTING
                    || state == ConnectState.STOP_TRYING)
                    return;
            }

            // connect with specified server name
            String serverAddressUserSetting
                = getAccountID().getAccountPropertyString(
                    ProtocolProviderFactory.SERVER_ADDRESS);

            int serverPort = getAccountID().getAccountPropertyInt(
                    ProtocolProviderFactory.SERVER_PORT, 5222);

            InetSocketAddress[] addrs = null;
            try
            {
                addrs = NetworkUtils.getAandAAAARecords(
                    serverAddressUserSetting,
                    serverPort
                );
            }
            catch (ParseException e)
            {
                logger.error("Domain not resolved", e);
            }
            catch (DnssecException e)
            {
                logger.error("DNSSEC failure for overridden server", e);
                setDnssecLoginFailure();
                return;
            }

            if (addrs == null || addrs.length == 0)
            {
                logger.error("No server addresses found");

                eventDuringLogin = null;

                fireRegistrationStateChanged(
                    getRegistrationState(),
                    RegistrationState.CONNECTION_FAILED,
                    RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND,
                    "No server addresses found");
            }
            else
            {
                for (InetSocketAddress isa : addrs)
                {
                    try
                    {
                        state = connectAndLogin(isa, serviceName,
                            loginStrategy);
                        if(state == ConnectState.ABORT_CONNECTING
                            || state == ConnectState.STOP_TRYING)
                            return;
                    }
                    catch(XMPPException ex)
                    {
                        disconnectAndCleanConnection();
                        if(isAuthenticationFailed(ex))
                            throw ex;
                    }
                }
            }
        }
    }

    /**
     * Creates the JabberLoginStrategy to use for the current account.
     */
    private JabberLoginStrategy createLoginStrategy()
    {
        if (((JabberAccountIDImpl)getAccountID()).isAnonymousAuthUsed())
        {
            return new AnonymousLoginStrategy(
                getAccountID().getAuthorizationName());
        }

        String clientCertId = getAccountID().getAccountPropertyString(
                ProtocolProviderFactory.CLIENT_TLS_CERTIFICATE);
        if(clientCertId != null)
        {
            return new LoginByClientCertificateStrategy(getAccountID());
        }
        else
        {
            return new LoginByPasswordStrategy(this, getAccountID());
        }
    }

    private void setDnssecLoginFailure()
    {
        eventDuringLogin = new RegistrationStateChangeEvent(
            this,
            getRegistrationState(),
            RegistrationState.UNREGISTERED,
            RegistrationStateChangeEvent.REASON_USER_REQUEST,
            "No usable host found due to DNSSEC failures");
    }

    /**
     * Connects using the domain specified and its SRV records.
     * @param domain the domain to use
     * @param serviceName the domain name of the user's login
     * @param dnssecState state of possible received DNSSEC exceptions
     * @param loginStrategy the login strategy to use
     * @return whether to continue trying or stop.
     */
    private ConnectState connectUsingSRVRecords(
        String domain,
        String serviceName,
        boolean[] dnssecState,
        JabberLoginStrategy loginStrategy)
        throws XMPPException
    {
        // check to see is there SRV records for this server domain
        SRVRecord srvRecords[] = null;
        try
        {
            srvRecords = NetworkUtils
                .getSRVRecords("xmpp-client", "tcp", domain);
        }
        catch (ParseException e)
        {
            logger.error("SRV record not resolved", e);
        }
        catch (DnssecException e)
        {
            logger.error("DNSSEC failure for SRV lookup", e);
            dnssecState[0] = true;
        }

        if(srvRecords != null)
        {
            for(SRVRecord srv : srvRecords)
            {
                InetSocketAddress[] addrs = null;
                try
                {
                    addrs =
                        NetworkUtils.getAandAAAARecords(
                            srv.getTarget(),
                            srv.getPort()
                        );
                }
                catch (ParseException e)
                {
                    logger.error("Invalid SRV record target", e);
                }
                catch (DnssecException e)
                {
                    logger.error("DNSSEC failure for A/AAAA lookup of SRV", e);
                    dnssecState[0] = true;
                }

                if (addrs == null || addrs.length == 0)
                {
                    logger.error("No A/AAAA addresses found for " +
                        srv.getTarget());
                    continue;
                }

                for (InetSocketAddress isa : addrs)
                {
                    try
                    {
                        // if failover mechanism is enabled, use it,
                        // default is not enabled.
                        if(JabberActivator.getConfigurationService()
                            .getBoolean(FailoverConnectionMonitor
                                                .REVERSE_FAILOVER_ENABLED_PROP,
                                false
                            ))
                        {
                            FailoverConnectionMonitor.getInstance(this)
                                .setCurrent(domain, srv.getTarget());
                        }

                        ConnectState state = connectAndLogin(
                            isa, serviceName, loginStrategy);
                        return state;
                    }
                    catch(XMPPException ex)
                    {
                        logger.error("Error connecting to " + isa
                            + " for domain:" + domain
                            + " serviceName:" + serviceName, ex);

                        disconnectAndCleanConnection();

                        if(isAuthenticationFailed(ex))
                            throw ex;
                    }
                }
            }
        }
        else
            logger.error("No SRV addresses found for _xmpp-client._tcp."
                + domain);

        return ConnectState.CONTINUE_TRYING;
    }

    /**
     * Tries to login to the XMPP server with the supplied user ID. If the
     * protocol is Google Talk, the user ID including the service name is used.
     * For other protocols, if the login with the user ID without the service
     * name fails, a second attempt including the service name is made.
     *
     * @param currentAddress the IP address to connect to
     * @param serviceName the domain name of the user's login
     * @param loginStrategy the login strategy to use
     * @throws XMPPException when a failure occurs
     */
    private ConnectState connectAndLogin(InetSocketAddress currentAddress,
        String serviceName,
        JabberLoginStrategy loginStrategy)
        throws XMPPException
    {
        String userID = null;
        boolean qualifiedUserID;

        /* with a google account (either gmail or google apps
         * related ones), the userID MUST be the full e-mail address
         * not just the ID
         */
        if(getAccountID().getProtocolDisplayName().equals("Google Talk"))
        {
            userID = getAccountID().getUserID();
            qualifiedUserID = true;
        }
        else
        {
            userID = StringUtils.parseName(getAccountID().getUserID());
            qualifiedUserID = false;
        }

        try
        {
            return connectAndLogin(
                currentAddress, serviceName,
                userID, resource, loginStrategy);
        }
        catch(XMPPException ex)
        {
            // server disconnect us after such an error, do cleanup
            disconnectAndCleanConnection();

            //no need to check with a different username if the
            //socket could not be opened
            if (ex.getWrappedThrowable() instanceof ConnectException
                || ex.getWrappedThrowable() instanceof NoRouteToHostException)
            {
                //as we got an exception not handled in connectAndLogin
                //no state was set, so fire it here so we can continue
                //with the re-register process
                //2013-08-07 do not fire event, if we have several
                // addresses and we fire event will activate reconnect
                // but we will continue connecting with other addresses
                // and can register with address, then unregister and try again
                // that is from reconnect plugin.
                // Storing event for fire after all have failed and we have
                // tried every address.
                eventDuringLogin = new RegistrationStateChangeEvent(
                    ProtocolProviderServiceJabberImpl.this,
                    getRegistrationState(),
                    RegistrationState.CONNECTION_FAILED,
                    RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND,
                    null);

                throw ex;
            }

            // don't attempt to append the service name if it's already there
            if (!qualifiedUserID)
            {
                try
                {
                    // logging in might need the service name
                    return connectAndLogin(
                        currentAddress, serviceName,
                        userID + "@" + serviceName,
                        resource,
                        loginStrategy);
                }
                catch(XMPPException ex2)
                {
                    disconnectAndCleanConnection();
                    throw ex; //throw the original exception
                }
            }
            else
                throw ex;
        }
    }

    /**
     * Initializes the Jabber Resource identifier.
     */
    private void loadResource()
    {
        if(resource == null)
        {
            String defaultResource = "jitsi";
            String autoGenenerateResource =
                getAccountID().getAccountPropertyString(
                    ProtocolProviderFactory.AUTO_GENERATE_RESOURCE);
            if(autoGenenerateResource == null ||
                Boolean.parseBoolean(autoGenenerateResource))
            {
                SecureRandom random = new SecureRandom();

                resource = defaultResource + "-" +
                    new BigInteger(32, random).toString(32);
            }
            else
            {
                resource = getAccountID().getAccountPropertyString(
                    ProtocolProviderFactory.RESOURCE);

                if(resource == null || resource.length() == 0)
                    resource = defaultResource;
            }
        }
    }

    /**
     * Sets the global proxy information based on the configuration
     *
     * @throws OperationFailedException
     */
    private void loadProxy() throws OperationFailedException
    {
        String globalProxyType =
            JabberActivator.getConfigurationService()
            .getString(ProxyInfo.CONNECTION_PROXY_TYPE_PROPERTY_NAME);
        if(globalProxyType == null ||
           globalProxyType.equals(ProxyInfo.ProxyType.NONE.name()))
        {
            proxy = org.jivesoftware.smack.proxy.ProxyInfo.forNoProxy();
        }
        else
        {
            String globalProxyAddress =
                JabberActivator.getConfigurationService().getString(
                ProxyInfo.CONNECTION_PROXY_ADDRESS_PROPERTY_NAME);
            String globalProxyPortStr =
                JabberActivator.getConfigurationService().getString(
                ProxyInfo.CONNECTION_PROXY_PORT_PROPERTY_NAME);
            int globalProxyPort;
            try
            {
                globalProxyPort = Integer.parseInt(
                    globalProxyPortStr);
            }
            catch(NumberFormatException ex)
            {
                throw new OperationFailedException("Wrong proxy port, "
                        + globalProxyPortStr
                        + " does not represent an integer",
                    OperationFailedException.INVALID_ACCOUNT_PROPERTIES,
                    ex);
            }
            String globalProxyUsername =
                JabberActivator.getConfigurationService().getString(
                ProxyInfo.CONNECTION_PROXY_USERNAME_PROPERTY_NAME);
            String globalProxyPassword =
                JabberActivator.getConfigurationService().getString(
                ProxyInfo.CONNECTION_PROXY_PASSWORD_PROPERTY_NAME);
            if(globalProxyAddress == null ||
                globalProxyAddress.length() <= 0)
            {
                throw new OperationFailedException(
                    "Missing Proxy Address",
                    OperationFailedException.INVALID_ACCOUNT_PROPERTIES);
            }
            try
            {
                proxy = new org.jivesoftware.smack.proxy.ProxyInfo(
                    Enum.valueOf(org.jivesoftware.smack.proxy.ProxyInfo.
                        ProxyType.class, globalProxyType),
                    globalProxyAddress, globalProxyPort,
                    globalProxyUsername, globalProxyPassword);
            }
            catch(IllegalArgumentException e)
            {
                logger.error("Invalid value for smack proxy enum", e);
                proxy = null;
            }
        }
    }

    /**
     * Connects xmpp connection and login. Returning the state whether is it
     * final - Abort due to certificate cancel or keep trying cause only current
     * address has failed or stop trying cause we succeeded.
     * @param address the address to connect to
     * @param serviceName the service name to use
     * @param userName the username to use
     * @param resource and the resource.
     * @param loginStrategy the login strategy to use
     * @return return the state how to continue the connect process.
     * @throws XMPPException if we cannot connect for some reason
     */
    private ConnectState connectAndLogin(
            InetSocketAddress address, String serviceName,
            String userName, String resource,
            JabberLoginStrategy loginStrategy)
        throws XMPPException
    {
        // BOSH or TCP ?
        ConnectionConfiguration confConn;
        String boshURL = accountID.getBoshUrl();
        boolean isBosh = !org.jitsi.util.StringUtils.isNullOrEmpty(boshURL);

        if (isBosh)
        {
            confConn = new BOSHConfiguration(serviceName);
            ((BOSHConfiguration)confConn).setBoshUrl(boshURL);
        }
        else
        {
            confConn
                = new ConnectionConfiguration(
                        address.getAddress().getHostAddress(),
                        address.getPort(),
                        serviceName, proxy);
        }

        // if we have OperationSetPersistentPresence skip sending initial
        // presence while login is executed, the OperationSet will take care
        // of it
        if(getOperationSet(OperationSetPersistentPresence.class) != null)
            confConn.setSendPresence(false);

        confConn.setReconnectionAllowed(false);
        boolean tlsRequired = loginStrategy.isTlsRequired();

        // user have the possibility to disable TLS but in this case, it will
        // not be able to connect to a server which requires TLS
        confConn.setSecurityMode(
            tlsRequired ? ConnectionConfiguration.SecurityMode.required :
                ConnectionConfiguration.SecurityMode.enabled);

        TLSUtils.setTLSOnly(confConn);

        if(connection != null)
        {
            logger.error("Connection is not null and isConnected:"
                + connection.isConnected(),
                new Exception("Trace possible duplicate connections: " +
                    getAccountID().getAccountAddress()));
            disconnectAndCleanConnection();
        }

        connection
            = isBosh
                ? new XMPPBOSHConnection((BOSHConfiguration)confConn)
                : new XMPPConnection(confConn);

        this.address = address;

        try
        {
            CertificateService cvs =
                getCertificateVerificationService();
            if(cvs != null)
            {
                SSLContext sslContext = loginStrategy.createSslContext(cvs,
                        getTrustManager(cvs, serviceName));

                // log SSL/TLS algorithms and protocols
                if (logger.isDebugEnabled() && sslContext != null)
                {
                    final StringBuilder buff = new StringBuilder();
                    buff.append("Available TLS protocols and algorithms:\n");
                    buff.append("Default protocols: ");
                    buff.append(Arrays.toString(
                        sslContext.getDefaultSSLParameters().getProtocols()));
                    buff.append("\n");
                    buff.append("Supported protocols: ");
                    buff.append(Arrays.toString(
                        sslContext.getSupportedSSLParameters().getProtocols()));
                    buff.append("\n");
                    buff.append("Default cipher suites: ");
                    buff.append(Arrays.toString(
                            sslContext.getDefaultSSLParameters()
                            .getCipherSuites()));
                    buff.append("\n");
                    buff.append("Supported cipher suites: ");
                    buff.append(Arrays.toString(
                            sslContext.getSupportedSSLParameters()
                            .getCipherSuites()));
                    logger.debug(buff.toString());
                }

                confConn.setCustomSSLContext(sslContext);
            }
            else if (tlsRequired)
                throw new XMPPException(
                    "Certificate verification service is "
                    + "unavailable and TLS is required");
        }
        catch(GeneralSecurityException e)
        {
            logger.error("Error creating custom trust manager", e);
            throw new XMPPException("Error creating custom trust manager", e);
        }

        // FIXME rework debugger to work with Connection if possible
        if(debugger == null && connection instanceof XMPPConnection)
        {
            debugger = new SmackPacketDebugger();

            // sets the debugger
            debugger.setConnection((XMPPConnection) connection);
            connection.addPacketListener(debugger, null);
            connection.addPacketInterceptor(debugger, null);
        }

        connection.connect();

        setTrafficClass();

        if(abortConnecting)
        {
            abortConnecting = false;
            disconnectAndCleanConnection();

            return ConnectState.ABORT_CONNECTING;
        }

        registerServiceDiscoveryManager();

        if(connectionListener == null)
        {
            connectionListener = new JabberConnectionListener();
        }

        if(!connection.isSecureConnection() && tlsRequired)
        {
            throw new XMPPException("TLS is required by client");
        }

        if(!connection.isConnected())
        {
            // connection is not connected, lets set state to our connection
            // as failed seems there is some lag/problem with network
            // and this way we will inform for it and later reconnect if needed
            // as IllegalStateException that is thrown within
            // addConnectionListener is not handled properly
            disconnectAndCleanConnection();

            logger.error("Connection not established, server not found!");

            eventDuringLogin = null;

            fireRegistrationStateChanged(getRegistrationState(),
                RegistrationState.CONNECTION_FAILED,
                RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND, null);

            return ConnectState.ABORT_CONNECTING;
        }
        else
        {
            final SSLSocket sslSocket = getSSLSocket();

            if (sslSocket != null)
            {
                StringBuilder buff = new StringBuilder();
                buff.append("Chosen TLS protocol and algorithm:\n")
                        .append("Protocol: ").append(sslSocket.getSession()
                        .getProtocol()).append("\n")
                        .append("Cipher suite: ").append(sslSocket.getSession()
                        .getCipherSuite());
                logger.info(buff.toString());

                if (logger.isDebugEnabled())
                {
                    buff = new StringBuilder();
                    buff.append("Server TLS certificate chain:\n");
                    try
                    {
                        buff.append(Arrays.toString(
                                sslSocket.getSession().getPeerCertificates()));
                    }
                    catch (SSLPeerUnverifiedException ex)
                    {
                        buff.append("<unavailable: ")
                                .append(ex.getLocalizedMessage()).append(">");
                    }
                    logger.debug(buff.toString());
                }
            }

            connection.addConnectionListener(connectionListener);
        }

        if(abortConnecting)
        {
            abortConnecting = false;
            disconnectAndCleanConnection();

            return ConnectState.ABORT_CONNECTING;
        }

        fireRegistrationStateChanged(
                getRegistrationState()
                , RegistrationState.REGISTERING
                , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED
                , null);

        if (!loginStrategy.login(connection, userName, resource))
        {
            disconnectAndCleanConnection();
            eventDuringLogin = null;
            fireRegistrationStateChanged(
                getRegistrationState(),
                // not auth failed, or there would be no info-popup
                RegistrationState.CONNECTION_FAILED,
                RegistrationStateChangeEvent.REASON_AUTHENTICATION_FAILED,
                loginStrategy.getClass().getName() + " requests abort");

            return ConnectState.ABORT_CONNECTING;
        }

        if(connection.isAuthenticated())
        {
            eventDuringLogin = null;

            fireRegistrationStateChanged(
                getRegistrationState(),
                RegistrationState.REGISTERED,
                RegistrationStateChangeEvent.REASON_NOT_SPECIFIED, null);

            return ConnectState.STOP_TRYING;
        }
        else
        {
            disconnectAndCleanConnection();

            eventDuringLogin = null;

            fireRegistrationStateChanged(
                getRegistrationState()
                , RegistrationState.UNREGISTERED
                , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED
                , null);

            return ConnectState.CONTINUE_TRYING;
        }
    }

    /**
     * Gets the TrustManager that should be used for the specified service
     *
     * @param serviceName the service name
     * @param cvs The CertificateVerificationService to retrieve the
     *            trust manager
     * @return the trust manager
     */
    private X509TrustManager getTrustManager(CertificateService cvs,
        String serviceName)
        throws GeneralSecurityException
    {
        return new HostTrustManager(
            cvs.getTrustManager(
                Arrays.asList(new String[]{
                        serviceName,
                        "_xmpp-client." + serviceName
                })
            )
        );
    }

    /**
     * Registers our ServiceDiscoveryManager
     */
    private void registerServiceDiscoveryManager()
    {
        // we setup supported features no packets are actually sent
        //during feature registration so we'd better do it here so that
        //our first presence update would contain a caps with the right
        //features.
        String name
            = System.getProperty(
                    "sip-communicator.application.name",
                    "SIP Communicator ")
                + System.getProperty("sip-communicator.version","SVN");

        ServiceDiscoveryManager.setIdentityName(name);
        ServiceDiscoveryManager.setIdentityType("pc");

        discoveryManager
            = new ScServiceDiscoveryManager(
                    this,
                    connection,
                    new String[] { "http://jabber.org/protocol/commands"},
                    // Add features Jitsi supports in addition to smack.
                    supportedFeatures.toArray(
                            new String[supportedFeatures.size()]),
                    true);

        /*
         * Expose the discoveryManager as service-public through the
         * OperationSetContactCapabilities of this ProtocolProviderService.
         */
        if (opsetContactCapabilities != null)
            opsetContactCapabilities.setDiscoveryManager(discoveryManager);
    }

    /**
     * Used to disconnect current connection and clean it.
     */
    public void disconnectAndCleanConnection()
    {
        if(connection != null)
        {
            connection.removeConnectionListener(connectionListener);

            // disconnect anyway cause it will clear any listeners
            // that maybe added even if its not connected
            try
            {
                OperationSetPersistentPresenceJabberImpl opSet =
                    (OperationSetPersistentPresenceJabberImpl)
                    this.getOperationSet(OperationSetPersistentPresence.class);

                Presence unavailablePresence =
                    new Presence(Presence.Type.unavailable);

                if(opSet != null
                    && !org.jitsi.util.StringUtils
                        .isNullOrEmpty(opSet.getCurrentStatusMessage()))
                {
                    unavailablePresence.setStatus(
                        opSet.getCurrentStatusMessage());
                }

                connection.disconnect(unavailablePresence);
            } catch (Exception e)
            {}

            connectionListener = null;
            connection = null;
            // make it null as it also holds a reference to the old connection
            // will be created again on new connection
            try
            {
                /*
                 * The discoveryManager is exposed as service-public by the
                 * OperationSetContactCapabilities of this
                 * ProtocolProviderService. No longer expose it because it's
                 * going away.
                 */
                if (opsetContactCapabilities != null)
                    opsetContactCapabilities.setDiscoveryManager(null);
            }
            finally
            {
                if(discoveryManager != null)
                {
                    discoveryManager.stop();
                    discoveryManager = null;
                }
            }
        }
    }

    /**
     * Ends the registration of this protocol provider with the service.
     */
    public void unregister()
    {
        unregisterInternal(true);
    }

    /**
     * Ends the registration of this protocol provider with the service.
     * @param userRequest is the unregister by user request.
     */
    public void unregister(boolean userRequest)
    {
        unregisterInternal(true, userRequest);
    }

    /**
     * Unregister and fire the event if requested
     * @param fireEvent boolean
     */
    public void unregisterInternal(boolean fireEvent)
    {
        unregisterInternal(fireEvent, false);
    }

    /**
     * Unregister and fire the event if requested
     * @param fireEvent boolean
     */
    public void unregisterInternal(boolean fireEvent, boolean userRequest)
    {
        synchronized(initializationLock)
        {
            if(fireEvent)
            {
                eventDuringLogin = null;
                fireRegistrationStateChanged(
                    getRegistrationState()
                    , RegistrationState.UNREGISTERING
                    , RegistrationStateChangeEvent.REASON_NOT_SPECIFIED
                    , null
                    , userRequest);
            }

            disconnectAndCleanConnection();

            RegistrationState currRegState = getRegistrationState();

            if(fireEvent)
            {
                eventDuringLogin = null;
                fireRegistrationStateChanged(
                    currRegState,
                    RegistrationState.UNREGISTERED,
                    RegistrationStateChangeEvent.REASON_USER_REQUEST, null,
                    userRequest);
            }
        }
    }

    /**
     * Returns the short name of the protocol that the implementation of this
     * provider is based upon (like SIP, Jabber, ICQ/AIM, or others for
     * example).
     *
     * @return a String containing the short name of the protocol this
     *   service is taking care of.
     */
    public String getProtocolName()
    {
        return ProtocolNames.JABBER;
    }

    /**
     * Initialized the service implementation, and puts it in a sate where it
     * could interoperate with other services. It is strongly recommended that
     * properties in this Map be mapped to property names as specified by
     * <tt>AccountProperties</tt>.
     *
     * @param screenname the account id/uin/screenname of the account that
     * we're about to create
     * @param accountID the identifier of the account that this protocol
     * provider represents.
     *
     * @see net.java.sip.communicator.service.protocol.AccountID
     */
    protected void initialize(String screenname,
                              JabberAccountID accountID)
    {
        synchronized(initializationLock)
        {
            this.accountID = accountID;

            // in case of modified account, we clear list of supported features
            // and every state change listeners, otherwise we can have two
            // OperationSet for same feature and it can causes problem (i.e.
            // two OperationSetBasicTelephony can launch two ICE negotiations
            // (with different ufrag/pwd) and peer will failed call. And
            // by the way user will see two dialog for answering/refusing the
            // call
            supportedFeatures.clear();
            this.clearRegistrationStateChangeListener();
            this.clearSupportedOperationSet();

            String protocolIconPath
                = accountID.getAccountPropertyString(
                        ProtocolProviderFactory.PROTOCOL_ICON_PATH);

            if (protocolIconPath == null)
                protocolIconPath = "resources/images/protocol/jabber";

            jabberIcon = new ProtocolIconJabberImpl(protocolIconPath);

            jabberStatusEnum
                = JabberStatusEnum.getJabberStatusEnum(protocolIconPath);

            //this feature is mandatory to be compliant with Service Discovery
            supportedFeatures.add("http://jabber.org/protocol/disco#info");

            String keepAliveStrValue
                = accountID.getAccountPropertyString(
                    ProtocolProviderFactory.KEEP_ALIVE_METHOD);

            InfoRetreiver infoRetreiver = new InfoRetreiver(this, screenname);

            //initialize the presence OperationSet
            OperationSetPersistentPresenceJabberImpl persistentPresence =
                new OperationSetPersistentPresenceJabberImpl(this, infoRetreiver);

            addSupportedOperationSet(
                OperationSetPersistentPresence.class,
                persistentPresence);
            // TODO: add the feature, if any, corresponding to persistent
            // presence, if someone knows
            // supportedFeatures.add(_PRESENCE_);

            //register it once again for those that simply need presence
            addSupportedOperationSet(
                OperationSetPresence.class,
                persistentPresence);

            if(accountID.getAccountPropertyString(
                    ProtocolProviderFactory.ACCOUNT_READ_ONLY_GROUPS) != null)
            {
                addSupportedOperationSet(
                    OperationSetPersistentPresencePermissions.class,
                    new OperationSetPersistentPresencePermissionsJabberImpl(
                            this));
            }

            //initialize the IM operation set
            OperationSetBasicInstantMessagingJabberImpl basicInstantMessaging =
                new OperationSetBasicInstantMessagingJabberImpl(this);

            if (keepAliveStrValue == null
                || keepAliveStrValue.equalsIgnoreCase("XEP-0199"))
            {
                if(keepAliveManager == null)
                    keepAliveManager = new KeepAliveManager(this);
            }

            addSupportedOperationSet(
                OperationSetBasicInstantMessaging.class,
                basicInstantMessaging);

            // The http://jabber.org/protocol/xhtml-im feature is included
            // already in smack.

            addSupportedOperationSet(
                OperationSetExtendedAuthorizations.class,
                new OperationSetExtendedAuthorizationsJabberImpl(
                    this,
                    persistentPresence));

            //initialize the Whiteboard operation set
            addSupportedOperationSet(
                OperationSetWhiteboarding.class,
                new OperationSetWhiteboardingJabberImpl(this));

            //initialize the typing notifications operation set
            addSupportedOperationSet(
                OperationSetTypingNotifications.class,
                new OperationSetTypingNotificationsJabberImpl(this));

            // The http://jabber.org/protocol/chatstates feature implemented in
            // OperationSetTypingNotifications is included already in smack.

            //initialize the multi user chat operation set
            addSupportedOperationSet(
                OperationSetMultiUserChat.class,
                new OperationSetMultiUserChatJabberImpl(this));

            addSupportedOperationSet(
                OperationSetJitsiMeetTools.class,
                new OperationSetJitsiMeetToolsJabberImpl(this));

            addSupportedOperationSet(
                OperationSetServerStoredContactInfo.class,
                new OperationSetServerStoredContactInfoJabberImpl(
                        infoRetreiver));

            OperationSetServerStoredAccountInfo accountInfo =
                new OperationSetServerStoredAccountInfoJabberImpl(this,
                        infoRetreiver,
                        screenname);

            addSupportedOperationSet(
                OperationSetServerStoredAccountInfo.class,
                accountInfo);

            // Initialize avatar operation set
            addSupportedOperationSet(
                OperationSetAvatar.class,
                new OperationSetAvatarJabberImpl(this, accountInfo));

            // initialize the file transfer operation set
            addSupportedOperationSet(
                OperationSetFileTransfer.class,
                new OperationSetFileTransferJabberImpl(this));

            addSupportedOperationSet(
                OperationSetInstantMessageTransform.class,
                new OperationSetInstantMessageTransformImpl());

            // Include features we're supporting in addition to the four
            // included by smack itself:
            // http://jabber.org/protocol/si/profile/file-transfer
            // http://jabber.org/protocol/si
            // http://jabber.org/protocol/bytestreams
            // http://jabber.org/protocol/ibb
            supportedFeatures.add("urn:xmpp:thumbs:0");
            supportedFeatures.add("urn:xmpp:bob");

            // initialize the thumbnailed file factory operation set
            addSupportedOperationSet(
                OperationSetThumbnailedFileFactory.class,
                new OperationSetThumbnailedFileFactoryImpl());

            // TODO: this is the "main" feature to advertise when a client
            // support muc. We have to add some features for
            // specific functionality we support in muc.
            // see http://www.xmpp.org/extensions/xep-0045.html

            // The http://jabber.org/protocol/muc feature is already included in
            // smack.
            supportedFeatures.add("http://jabber.org/protocol/muc#rooms");
            supportedFeatures.add("http://jabber.org/protocol/muc#traffic");

            // RTP HDR extension
            supportedFeatures.add(URN_XMPP_JINGLE_RTP_HDREXT);

            ProviderManager providerManager
                = ProtocolProviderFactoryJabberImpl.providerManager;

            //register our jingle provider
            providerManager.addIQProvider( JingleIQ.ELEMENT_NAME,
                                           JingleIQ.NAMESPACE,
                                           new JingleIQProvider());

            // register our input event provider
            providerManager.addIQProvider(InputEvtIQ.ELEMENT_NAME,
                                          InputEvtIQ.NAMESPACE,
                                          new InputEvtIQProvider());

            // register our coin provider
            providerManager.addIQProvider(CoinIQ.ELEMENT_NAME,
                                          CoinIQ.NAMESPACE,
                                          new CoinIQProvider());
            supportedFeatures.add(URN_XMPP_JINGLE_COIN);

            // register our JingleInfo provider
            providerManager.addIQProvider(JingleInfoQueryIQ.ELEMENT_NAME,
                                          JingleInfoQueryIQ.NAMESPACE,
                                          new JingleInfoQueryIQProvider());

            // Jitsi Videobridge IQProvider and PacketExtensionProvider
            providerManager.addIQProvider(
                    ColibriConferenceIQ.ELEMENT_NAME,
                    ColibriConferenceIQ.NAMESPACE,
                    new ColibriIQProvider());

            providerManager.addIQProvider(
                    JibriIq.ELEMENT_NAME,
                    JibriIq.NAMESPACE,
                    new JibriIqProvider()
            );

            providerManager.addExtensionProvider(
                    ConferenceDescriptionPacketExtension.ELEMENT_NAME,
                    ConferenceDescriptionPacketExtension.NAMESPACE,
                    new ConferenceDescriptionPacketExtension.Provider());

            providerManager.addExtensionProvider(
                CarbonPacketExtension.RECEIVED_ELEMENT_NAME,
                CarbonPacketExtension.NAMESPACE,
                new CarbonPacketExtension.Provider(
                    CarbonPacketExtension.RECEIVED_ELEMENT_NAME));

            providerManager.addExtensionProvider(
                CarbonPacketExtension.SENT_ELEMENT_NAME,
                CarbonPacketExtension.NAMESPACE,
                new CarbonPacketExtension.Provider(
                    CarbonPacketExtension.SENT_ELEMENT_NAME));

            providerManager.addExtensionProvider(
                Nick.ELEMENT_NAME,
                Nick.NAMESPACE,
                new Nick.Provider());


            //initialize the telephony operation set
            boolean isCallingDisabled
                = JabberActivator.getConfigurationService()
                    .getBoolean(IS_CALLING_DISABLED, false);

            boolean isCallingDisabledForAccount
                = accountID.getAccountPropertyBoolean(
                    ProtocolProviderFactory.IS_CALLING_DISABLED_FOR_ACCOUNT,
                    false);

            // Check if calling is enabled.
            if (!isCallingDisabled && !isCallingDisabledForAccount)
            {
                OperationSetBasicTelephonyJabberImpl basicTelephony
                    = new OperationSetBasicTelephonyJabberImpl(this);

                addSupportedOperationSet(
                    OperationSetAdvancedTelephony.class,
                    basicTelephony);
                addSupportedOperationSet(
                    OperationSetBasicTelephony.class,
                    basicTelephony);
                addSupportedOperationSet(
                    OperationSetSecureZrtpTelephony.class,
                    basicTelephony);
                addSupportedOperationSet(
                    OperationSetSecureSDesTelephony.class,
                    basicTelephony);

                // initialize video telephony OperationSet
                addSupportedOperationSet(
                    OperationSetVideoTelephony.class,
                    new OperationSetVideoTelephonyJabberImpl(basicTelephony));

                addSupportedOperationSet(
                    OperationSetTelephonyConferencing.class,
                    new OperationSetTelephonyConferencingJabberImpl(this));

                addSupportedOperationSet(
                    OperationSetBasicAutoAnswer.class,
                    new OperationSetAutoAnswerJabberImpl(this));

                addSupportedOperationSet(
                    OperationSetResourceAwareTelephony.class,
                    new OperationSetResAwareTelephonyJabberImpl(basicTelephony));

                // Only init video bridge if enabled
                boolean isVideobridgeDisabled
                    = JabberActivator.getConfigurationService()
                      .getBoolean(OperationSetVideoBridge.
                          IS_VIDEO_BRIDGE_DISABLED, false);

                if (!isVideobridgeDisabled)
                {
                    // init video bridge
                    addSupportedOperationSet(
                        OperationSetVideoBridge.class,
                        new OperationSetVideoBridgeImpl(this));
                }

                // init DTMF
                OperationSetDTMFJabberImpl operationSetDTMF
                    = new OperationSetDTMFJabberImpl(this);
                addSupportedOperationSet(
                    OperationSetDTMF.class, operationSetDTMF);

                addSupportedOperationSet(
                    OperationSetIncomingDTMF.class,
                    new OperationSetIncomingDTMFJabberImpl());

                addJingleFeatures();

                // Check if desktop streaming is enabled.
                boolean isDesktopStreamingDisabled
                    = JabberActivator.getConfigurationService()
                        .getBoolean(IS_DESKTOP_STREAMING_DISABLED, false);

                boolean isAccountDesktopStreamingDisabled
                    = accountID.getAccountPropertyBoolean(
                        ProtocolProviderFactory.IS_DESKTOP_STREAMING_DISABLED,
                        false);

                if (!isDesktopStreamingDisabled
                    && !isAccountDesktopStreamingDisabled)
                {
                    // initialize desktop streaming OperationSet
                    addSupportedOperationSet(
                            OperationSetDesktopStreaming.class,
                            new OperationSetDesktopStreamingJabberImpl(
                                basicTelephony));

                    if(!accountID.getAccountPropertyBoolean(
                        ProtocolProviderFactory
                            .IS_DESKTOP_REMOTE_CONTROL_DISABLED,
                        false))
                    {
                        // initialize desktop sharing OperationSets
                        addSupportedOperationSet(
                            OperationSetDesktopSharingServer.class,
                            new OperationSetDesktopSharingServerJabberImpl(
                                basicTelephony));

                        // Adds extension to support remote control as a sharing
                        // server (sharer).
                        supportedFeatures.add(InputEvtIQ.NAMESPACE_SERVER);

                        addSupportedOperationSet(
                            OperationSetDesktopSharingClient.class,
                            new OperationSetDesktopSharingClientJabberImpl(this)
                            );
                        // Adds extension to support remote control as a sharing
                        // client (sharer).
                        supportedFeatures.add(InputEvtIQ.NAMESPACE_CLIENT);
                    }
                }
            }

            // OperationSetContactCapabilities
            opsetContactCapabilities
                = new OperationSetContactCapabilitiesJabberImpl(this);
            if (discoveryManager != null)
                opsetContactCapabilities.setDiscoveryManager(discoveryManager);
            addSupportedOperationSet(
                OperationSetContactCapabilities.class,
                opsetContactCapabilities);

            addSupportedOperationSet(
                OperationSetGenericNotifications.class,
                new OperationSetGenericNotificationsJabberImpl(this));

            supportedFeatures.add("jabber:iq:version");
            if(versionManager == null)
                versionManager = new VersionManager(this);

            supportedFeatures.add(MessageCorrectionExtension.NAMESPACE);
            addSupportedOperationSet(OperationSetMessageCorrection.class,
                    basicInstantMessaging);

            OperationSetChangePassword opsetChangePassword
                    = new OperationSetChangePasswordJabberImpl(this);
            addSupportedOperationSet(OperationSetChangePassword.class,
                    opsetChangePassword);

            OperationSetCusaxUtils opsetCusaxCusaxUtils
                    = new OperationSetCusaxUtilsJabberImpl(this);
            addSupportedOperationSet(OperationSetCusaxUtils.class,
                    opsetCusaxCusaxUtils);

            boolean isUserSearchEnabled = accountID.getAccountPropertyBoolean(
                IS_USER_SEARCH_ENABLED_PROPERTY, false);
            if(isUserSearchEnabled)
            {
                addSupportedOperationSet(OperationSetUserSearch.class,
                    new OperationSetUserSearchJabberImpl(this));
            }

            OperationSetTLS opsetTLS
                    = new OperationSetTLSJabberImpl(this);
            addSupportedOperationSet(OperationSetTLS.class,
                    opsetTLS);

            OperationSetConnectionInfo opsetConnectionInfo
                    = new OperationSetConnectionInfoJabberImpl();
            addSupportedOperationSet(OperationSetConnectionInfo.class,
                    opsetConnectionInfo);

            isInitialized = true;
        }
    }

    /**
     * Adds Jingle related features to the supported features.
     */
    private void addJingleFeatures()
    {
        // Add Jingle features to supported features.
        supportedFeatures.add(URN_XMPP_JINGLE);
        supportedFeatures.add(URN_XMPP_JINGLE_RTP);
        supportedFeatures.add(URN_XMPP_JINGLE_RAW_UDP_0);

        /*
         * Reflect the preference of the user with respect to the use of
         * ICE.
         */
        if (accountID.getAccountPropertyBoolean(
                ProtocolProviderFactory.IS_USE_ICE,
                true))
        {
            supportedFeatures.add(URN_XMPP_JINGLE_ICE_UDP_1);
        }

        supportedFeatures.add(URN_XMPP_JINGLE_RTP_AUDIO);
        supportedFeatures.add(URN_XMPP_JINGLE_RTP_VIDEO);
        supportedFeatures.add(URN_XMPP_JINGLE_RTP_ZRTP);

        /*
         * Reflect the preference of the user with respect to the use of
         * Jingle Nodes.
         */
        if (accountID.getAccountPropertyBoolean(
                ProtocolProviderFactoryJabberImpl.IS_USE_JINGLE_NODES,
                true))
        {
            supportedFeatures.add(URN_XMPP_JINGLE_NODES);
        }

        // XEP-0251: Jingle Session Transfer
        supportedFeatures.add(URN_XMPP_JINGLE_TRANSFER_0);

        // XEP-0320: Use of DTLS-SRTP in Jingle Sessions
        if (accountID.getAccountPropertyBoolean(
                    ProtocolProviderFactory.DEFAULT_ENCRYPTION,
                    true)
                && accountID.isEncryptionProtocolEnabled(
                        SrtpControlType.DTLS_SRTP))
        {
            supportedFeatures.add(URN_XMPP_JINGLE_DTLS_SRTP);
        }
    }

    /**
     * Makes the service implementation close all open sockets and release
     * any resources that it might have taken and prepare for
     * shutdown/garbage collection.
     */
    public void shutdown()
    {
        synchronized(initializationLock)
        {
            if (logger.isTraceEnabled())
                logger.trace("Killing the Jabber Protocol Provider.");

            //kill all active calls
            OperationSetBasicTelephonyJabberImpl telephony
                = (OperationSetBasicTelephonyJabberImpl)getOperationSet(
                    OperationSetBasicTelephony.class);
            if (telephony != null)
            {
                telephony.shutdown();
            }

            disconnectAndCleanConnection();

            isInitialized = false;
        }
    }

    /**
     * Returns true if the provider service implementation is initialized and
     * ready for use by other services, and false otherwise.
     *
     * @return true if the provider is initialized and ready for use and false
     * otherwise
     */
    public boolean isInitialized()
    {
        return isInitialized;
    }

    /**
     * Returns the AccountID that uniquely identifies the account represented
     * by this instance of the ProtocolProviderService.
     * @return the id of the account represented by this provider.
     */
    public AccountID getAccountID()
    {
        return accountID;
    }

    /**
     * Validates the node part of a JID and returns an error message if
     * applicable and a suggested correction.
     * 
     * @param contactId the contact identifier to validate
     * @param result Must be supplied as an empty a list. Implementors add
     *            items:
     *            <ol>
     *            <li>is the error message if applicable
     *            <li>a suggested correction. Index 1 is optional and can only
     *            be present if there was a validation failure.
     *            </ol>
     * @return true if the contact id is valid, false otherwise
     */
    @Override
    public boolean validateContactAddress(String contactId, List<String> result)
    {
        if (result == null)
        {
            throw new IllegalArgumentException("result must be an empty list");
        }

        result.clear();
        try
        {
            contactId = contactId.trim();
            if (contactId.length() == 0)
            {
                result.add(JabberActivator.getResources().getI18NString(
                    "impl.protocol.jabber.INVALID_ADDRESS", new String[]
                { contactId }));
                // no suggestion for an empty id
                return false;
            }

            String user = contactId;
            String remainder = "";
            int at = contactId.indexOf('@');
            if (at > -1)
            {
                user = contactId.substring(0, at);
                remainder = contactId.substring(at);
            }

            // <conforming-char> ::= #x21 | [#x23-#x25] | [#x28-#x2E] |
            // [#x30-#x39] | #x3B | #x3D | #x3F |
            // [#x41-#x7E] | [#x80-#xD7FF] |
            // [#xE000-#xFFFD] | [#x10000-#x10FFFF]
            boolean valid = true;
            String suggestion = "";
            for (char c : user.toCharArray())
            {
                if (!(c == 0x21 || (c >= 0x23 && c <= 0x25)
                    || (c >= 0x28 && c <= 0x2e) || (c >= 0x30 && c <= 0x39)
                    || c == 0x3b || c == 0x3d || c == 0x3f
                    || (c >= 0x41 && c <= 0x7e) || (c >= 0x80 && c <= 0xd7ff)
                    || (c >= 0xe000 && c <= 0xfffd)))
                {
                    valid = false;
                }
                else
                {
                    suggestion += c;
                }
            }

            if (!valid)
            {
                result.add(JabberActivator.getResources().getI18NString(
                    "impl.protocol.jabber.INVALID_ADDRESS", new String[]
                { contactId }));
                result.add(suggestion + remainder);
                return false;
            }

            return true;
        }
        catch (Exception ex)
        {
            result.add(JabberActivator.getResources().getI18NString(
                "impl.protocol.jabber.INVALID_ADDRESS", new String[]
            { contactId }));
        }

        return false;
    }

    /**
     * Returns the <tt>Connection</tt>opened by this provider
     * @return a reference to the <tt>Connection</tt> last opened by this
     * provider.
     */
    public Connection getConnection()
    {
        return connection;
    }

    /**
     * Determines whether a specific <tt>XMPPException</tt> signals that
     * attempted authentication has failed.
     *
     * @param ex the <tt>XMPPException</tt> which is to be determined whether it
     * signals that attempted authentication has failed
     * @return <tt>true</tt> if the specified <tt>ex</tt> signals that attempted
     * authentication has failed; otherwise, <tt>false</tt>
     */
    private boolean isAuthenticationFailed(XMPPException ex)
    {
        String exMsg = ex.getMessage().toLowerCase();

        // as there are no types or reasons for XMPPException
        // we try determine the reason according to their message
        // all messages that were found in smack 3.1.0 were took in count
        return
            ((exMsg.indexOf("sasl authentication") != -1)
                    && (exMsg.indexOf("failed") != -1))
            || (exMsg.indexOf(
                    "does not support compatible authentication mechanism")
                        != -1)
            || (exMsg.indexOf("unable to determine password") != -1);
    }

    /**
     * Tries to determine the appropriate message and status to fire,
     * according the exception.
     *
     * @param ex the {@link XMPPException} that caused the state change.
     */
    private void fireRegistrationStateChanged(XMPPException ex)
    {
        int reason = RegistrationStateChangeEvent.REASON_NOT_SPECIFIED;
        RegistrationState regState = RegistrationState.UNREGISTERED;
        String reasonStr = null;

        Throwable wrappedEx = ex.getWrappedThrowable();
        if(wrappedEx != null
            && (wrappedEx instanceof UnknownHostException
                || wrappedEx instanceof ConnectException
                || wrappedEx instanceof SocketException))
        {
            reason = RegistrationStateChangeEvent.REASON_SERVER_NOT_FOUND;
            regState = RegistrationState.CONNECTION_FAILED;
        }
        else
        {
            String exMsg = ex.getMessage().toLowerCase();

            // as there are no types or reasons for XMPPException
            // we try determine the reason according to their message
            // all messages that were found in smack 3.1.0 were took in count
            if(isAuthenticationFailed(ex))
            {
                JabberActivator.getProtocolProviderFactory().
                    storePassword(getAccountID(), null);

                reason = RegistrationStateChangeEvent
                    .REASON_AUTHENTICATION_FAILED;

                regState = RegistrationState.AUTHENTICATION_FAILED;

                fireRegistrationStateChanged(
                    getRegistrationState(), regState, reason, null);

                // Try to reregister and to ask user for a new password.
                reregister(SecurityAuthority.WRONG_PASSWORD);

                return;
            }
            else if(exMsg.indexOf("no response from the server") != -1
                || exMsg.indexOf("connection failed") != -1)
            {
                reason = RegistrationStateChangeEvent.REASON_NOT_SPECIFIED;
                regState = RegistrationState.CONNECTION_FAILED;
            }
            else if(exMsg.indexOf("tls is required") != -1)
            {
                regState = RegistrationState.AUTHENTICATION_FAILED;
                reason = RegistrationStateChangeEvent.REASON_TLS_REQUIRED;
            }
        }

        if(regState == RegistrationState.UNREGISTERED
            || regState == RegistrationState.CONNECTION_FAILED)
        {
            // we fired that for some reason we are going offline
            // lets clean the connection state for any future connections
            disconnectAndCleanConnection();
        }

        fireRegistrationStateChanged(
            getRegistrationState(), regState, reason, reasonStr);
    }

    /**
     * Enable to listen for jabber connection events
     */
    private class JabberConnectionListener
        implements ConnectionListener
    {
        /**
         * Implements <tt>connectionClosed</tt> from <tt>ConnectionListener</tt>
         */
        public void connectionClosed()
        {
            // if we are in the middle of connecting process
            // do not fire events, will do it later when the method
            // connectAndLogin finishes its work
            synchronized(connectAndLoginLock)
            {
                if(inConnectAndLogin)
                {
                    eventDuringLogin = new RegistrationStateChangeEvent(
                        ProtocolProviderServiceJabberImpl.this,
                        getRegistrationState(),
                        RegistrationState.CONNECTION_FAILED,
                        RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
                        null);
                     return;
                }
            }
            // fire that a connection failed, the reconnection mechanism
            // will look after us and will clean us, other wise we can do
            // a dead lock (connection closed is called
            // within xmppConneciton and calling disconnect again can lock it)
            fireRegistrationStateChanged(
                getRegistrationState(),
                RegistrationState.CONNECTION_FAILED,
                RegistrationStateChangeEvent.REASON_NOT_SPECIFIED,
                null);
        }

        /**
         * Implements <tt>connectionClosedOnError</tt> from
         * <tt>ConnectionListener</tt>.
         *
         * @param exception contains information on the error.
         */
        public void connectionClosedOnError(Exception exception)
        {
            logger.error("connectionClosedOnError " +
                         exception.getLocalizedMessage(), exception);

            int reason = RegistrationStateChangeEvent.REASON_NOT_SPECIFIED;

            if(exception instanceof XMPPException)
            {
                StreamError err = ((XMPPException)exception).getStreamError();

                if(err != null && err.getCode().equals(
                    XMPPError.Condition.conflict.toString()))
                {
                    // if we are in the middle of connecting process
                    // do not fire events, will do it later when the method
                    // connectAndLogin finishes its work
                    synchronized(connectAndLoginLock)
                    {
                        if(inConnectAndLogin)
                        {
                            eventDuringLogin = new RegistrationStateChangeEvent(
                                ProtocolProviderServiceJabberImpl.this,
                                getRegistrationState(),
                                RegistrationState.UNREGISTERED,
                                RegistrationStateChangeEvent.REASON_MULTIPLE_LOGINS,
                                "Connecting multiple times with the same resource");
                             return;
                        }
                    }

                    disconnectAndCleanConnection();

                    fireRegistrationStateChanged(getRegistrationState(),
                        RegistrationState.UNREGISTERED,
                        RegistrationStateChangeEvent.REASON_MULTIPLE_LOGINS,
                        "Connecting multiple times with the same resource");

                    return;
                }
            } // Ignore certificate exceptions as we handle them elsewhere
            else if(exception instanceof SSLHandshakeException &&
                exception.getCause() instanceof CertificateException)
            {
                return;
            }
            else if(exception instanceof XmlPullParserException)
            {
                reason = RegistrationStateChangeEvent
                    .REASON_SERVER_RETURNED_ERRONEOUS_INPUT;
            }

            // if we are in the middle of connecting process
            // do not fire events, will do it later when the method
            // connectAndLogin finishes its work
            synchronized(connectAndLoginLock)
            {
                if(inConnectAndLogin)
                {
                    eventDuringLogin = new RegistrationStateChangeEvent(
                        ProtocolProviderServiceJabberImpl.this,
                        getRegistrationState(),
                        RegistrationState.CONNECTION_FAILED,
                        reason,
                        exception.getMessage());
                     return;
                }
            }

            disconnectAndCleanConnection();

            fireRegistrationStateChanged(getRegistrationState(),
                RegistrationState.CONNECTION_FAILED,
                reason,
                exception.getMessage());
        }

        /**
         * Implements <tt>reconnectingIn</tt> from <tt>ConnectionListener</tt>
         *
         * @param i delay in seconds for reconnection.
         */
        public void reconnectingIn(int i)
        {
            if (logger.isInfoEnabled())
                logger.info("reconnectingIn " + i);
        }

        /**
         * Implements <tt>reconnectingIn</tt> from <tt>ConnectionListener</tt>
         */
        public void reconnectionSuccessful()
        {
            if (logger.isInfoEnabled())
                logger.info("reconnectionSuccessful");
        }

        /**
         * Implements <tt>reconnectionFailed</tt> from
         * <tt>ConnectionListener</tt>.
         *
         * @param exception description of the failure
         */
        public void reconnectionFailed(Exception exception)
        {
            if (logger.isInfoEnabled())
                logger.info("reconnectionFailed ", exception);
        }
    }

    /**
     * Returns the jabber protocol icon.
     * @return the jabber protocol icon
     */
    public ProtocolIcon getProtocolIcon()
    {
        return jabberIcon;
    }

    /**
     * Returns the current instance of <tt>JabberStatusEnum</tt>.
     *
     * @return the current instance of <tt>JabberStatusEnum</tt>.
     */
    JabberStatusEnum getJabberStatusEnum()
    {
        return jabberStatusEnum;
    }

    /**
     * Determines if the given list of <tt>features</tt> is supported by the
     * specified jabber id.
     *
     * @param jid the jabber id for which to check
     * @param features the list of features to check for
     *
     * @return <tt>true</tt> if the list of features is supported; otherwise,
     * <tt>false</tt>
     */
    public boolean isFeatureListSupported(String jid, String... features)
    {
        try
        {
            if(discoveryManager == null)
                return false;

            DiscoverInfo featureInfo =
                discoveryManager.discoverInfoNonBlocking(jid);

            if(featureInfo == null)
                return false;

            for (String feature : features)
            {
                if (!featureInfo.containsFeature(feature))
                {
                    // If one is not supported we return false and don't check
                    // the others.
                    return false;
                }
            }

            return true;
        }
        catch (XMPPException e)
        {
            if (logger.isDebugEnabled())
                logger.debug("Failed to retrive discovery info.", e);
        }

        return false;
    }

    /**
     * Determines if the given list of <tt>features</tt> is supported by the
     * specified jabber id.
     *
     * @param jid the jabber id that we'd like to get information about
     * @param feature the feature to check for
     *
     * @return <tt>true</tt> if the list of features is supported, otherwise
     * returns <tt>false</tt>
     */
    public boolean isFeatureSupported(String jid, String feature)
    {
        return isFeatureListSupported(jid, feature);
    }

    /**
     * Returns the full jabber id (jid) corresponding to the given contact. If
     * the provider is not connected returns null.
     *
     * @param contact the contact, for which we're looking for a jid
     * @return the jid of the specified contact or null if the provider is not
     * yet connected;
     */
    public String getFullJid(Contact contact)
    {
        return getFullJid(contact.getAddress());
    }

    /**
     * Returns the full jabber id (jid) corresponding to the given bare jid. If
     * the provider is not connected returns null.
     *
     * @param bareJid the bare contact address (i.e. no resource) whose full
     * jid we are looking for.
     * @return the jid of the specified contact or null if the provider is not
     * yet connected;
     */
    public String getFullJid(String bareJid)
    {
        Connection connection = getConnection();

        // when we are not connected there is no full jid
        if (connection != null && connection.isConnected())
        {
            Roster roster = connection.getRoster();

            if (roster != null)
                return roster.getPresence(bareJid).getFrom();
        }
        return null;
    }

    /**
     * The trust manager which asks the client whether to trust particular
     * certificate which is not globally trusted.
     */
    private class HostTrustManager
        implements X509TrustManager
    {
        /**
         * The default trust manager.
         */
        private final X509TrustManager tm;

        /**
         * Creates the custom trust manager.
         * @param tm the default trust manager.
         */
        HostTrustManager(X509TrustManager tm)
        {
            this.tm = tm;
        }

        /**
         * Not used.
         *
         * @return nothing.
         */
        public X509Certificate[] getAcceptedIssuers()
        {
            return new X509Certificate[0];
        }

        /**
         * Not used.
         * @param chain the cert chain.
         * @param authType authentication type like: RSA.
         * @throws CertificateException never
         * @throws UnsupportedOperationException always
         */
        public void checkClientTrusted(X509Certificate[] chain, String authType)
            throws CertificateException, UnsupportedOperationException
        {
            throw new UnsupportedOperationException();
        }

        /**
         * Check whether a certificate is trusted, if not as user whether he
         * trust it.
         * @param chain the certificate chain.
         * @param authType authentication type like: RSA.
         * @throws CertificateException not trusted.
         */
        public void checkServerTrusted(X509Certificate[] chain, String authType)
            throws CertificateException
        {
            abortConnecting = true;
            try
            {
                tm.checkServerTrusted(chain, authType);
            }
            catch(CertificateException e)
            {
                // notify in a separate thread to avoid a deadlock when a
                // reg state listener accesses a synchronized XMPPConnection
                // method (like getRoster)
                new Thread(new Runnable()
                {
                    public void run()
                    {
                        fireRegistrationStateChanged(getRegistrationState(),
                            RegistrationState.UNREGISTERED,
                            RegistrationStateChangeEvent.REASON_USER_REQUEST,
                            "Not trusted certificate");
                    }
                }).start();
                throw e;
            }

            if(abortConnecting)
            {
                // connect hasn't finished we will continue normally
                abortConnecting = false;
                return;
            }
            else
            {
                // in this situation connect method has finished
                // and it was disconnected so we wont to connect.

                // register.connect in new thread so we can release the
                // current connecting thread, otherwise this blocks
                // jabber
                new Thread(new Runnable()
                {
                    public void run()
                    {
                        reregister(SecurityAuthority.CONNECTION_FAILED);
                    }
                }).start();
                return;
            }
        }
    }

    /**
     * Returns the currently valid {@link ScServiceDiscoveryManager}.
     *
     * @return the currently valid {@link ScServiceDiscoveryManager}.
     */
    public ScServiceDiscoveryManager getDiscoveryManager()
    {
        return discoveryManager;
    }

    /**
     * Returns our own Jabber ID.
     *
     * @return our own Jabber ID.
     */
    public String getOurJID()
    {
        String jid = null;

        if (connection != null)
            jid = connection.getUser();

        if (jid == null)
        {
            // seems like the connection is not yet initialized so lets try to
            // construct our jid ourselves.
            String accountIDUserID = getAccountID().getUserID();
            String userID = StringUtils.parseName(accountIDUserID);
            String serviceName = StringUtils.parseServer(accountIDUserID);

            jid = userID + "@" + serviceName;
        }

        return jid;
    }

    /**
     * Returns the <tt>InetAddress</tt> that is most likely to be to be used
     * as a next hop when contacting our XMPP server. This is an utility method
     * that is used whenever we have to choose one of our local addresses (e.g.
     * when trying to pick a best candidate for raw udp). It is based on the
     * assumption that, in absence of any more specific details, chances are
     * that we will be accessing remote destinations via the same interface
     * that we are using to access our jabber server.
     *
     * @return the <tt>InetAddress</tt> that is most likely to be to be used
     * as a next hop when contacting our server.
     *
     * @throws IllegalArgumentException if we don't have a valid server.
     */
    public InetAddress getNextHop()
        throws IllegalArgumentException
    {
        InetAddress nextHop = null;
        String nextHopStr = null;

        if ( proxy != null
            && proxy.getProxyType()
              != org.jivesoftware.smack.proxy.ProxyInfo.ProxyType.NONE)
        {
            nextHopStr = proxy.getProxyAddress();
        }
        else
        {
            nextHopStr = getConnection().getHost();
        }

        try
        {
            nextHop = NetworkUtils.getInetAddress(nextHopStr);
        }
        catch (UnknownHostException ex)
        {
            throw new IllegalArgumentException(
                "seems we don't have a valid next hop.", ex);
        }

        if(logger.isDebugEnabled())
            logger.debug("Returning address " + nextHop + " as next hop.");

        return nextHop;
    }

    /**
     * Start auto-discovery of JingleNodes tracker/relays.
     */
    public void startJingleNodesDiscovery()
    {
        if (!(connection instanceof XMPPConnection))
        {
            logger.warn(
                "Jingle node discovery currently will work only with " +
                    "TCP XMPP connection");
            return;
        }

        // Jingle Nodes Service Initialization
        final XMPPConnection xmppConnection = (XMPPConnection) connection;
        final JabberAccountIDImpl accID = (JabberAccountIDImpl)getAccountID();
        final SmackServiceNode service
            = new SmackServiceNode(xmppConnection, 60000);
        // make sure SmackServiceNode will clean up when connection is closed
        xmppConnection.addConnectionListener(service);

        for(JingleNodeDescriptor desc : accID.getJingleNodes())
        {
            TrackerEntry entry = new TrackerEntry(
                    desc.isRelaySupported() ? TrackerEntry.Type.relay :
                        TrackerEntry.Type.tracker,
                    TrackerEntry.Policy._public,
                    desc.getJID(),
                    JingleChannelIQ.UDP);

            service.addTrackerEntry(entry);
        }

        new Thread(new JingleNodesServiceDiscovery(
                            service,
                            xmppConnection,
                            accID,
                            jingleNodesSyncRoot))
                .start();

        jingleNodesServiceNode = service;
    }

    /**
     * Get the Jingle Nodes service. Note that this method will block until
     * Jingle Nodes auto discovery (if enabled) finished.
     *
     * @return Jingle Nodes service
     */
    public SmackServiceNode getJingleNodesServiceNode()
    {
        synchronized(jingleNodesSyncRoot)
        {
            return jingleNodesServiceNode;
        }
    }

    /**
     * Logs a specific message and associated <tt>Throwable</tt> cause as an
     * error using the current <tt>Logger</tt> and then throws a new
     * <tt>OperationFailedException</tt> with the message, a specific error code
     * and the cause.
     *
     * @param message the message to be logged and then wrapped in a new
     * <tt>OperationFailedException</tt>
     * @param errorCode the error code to be assigned to the new
     * <tt>OperationFailedException</tt>
     * @param cause the <tt>Throwable</tt> that has caused the necessity to log
     * an error and have a new <tt>OperationFailedException</tt> thrown
     * @param logger the logger that we'd like to log the error <tt>message</tt>
     * and <tt>cause</tt>.
     *
     * @throws OperationFailedException the exception that we wanted this method
     * to throw.
     */
    public static void throwOperationFailedException( String    message,
                                                      int       errorCode,
                                                      Throwable cause,
                                                      Logger    logger)
        throws OperationFailedException
    {
        logger.error(message, cause);

        if(cause == null)
            throw new OperationFailedException(message, errorCode);
        else
            throw new OperationFailedException(message, errorCode, cause);
    }

    /**
     * Used when we need to re-register or someone needs to obtain credentials.
     * @return the SecurityAuthority.
     */
    public SecurityAuthority getAuthority()
    {
        return authority;
    }

    UserCredentials getUserCredentials()
    {
        return userCredentials;
    }

    /**
     * Returns true if our account is a Gmail or a Google Apps ones.
     *
     * @return true if our account is a Gmail or a Google Apps ones.
     */
    public boolean isGmailOrGoogleAppsAccount()
    {
        String domain = StringUtils.parseServer(
            getAccountID().getUserID());
        return isGmailOrGoogleAppsAccount(domain);
    }

    /**
     * Returns true if our account is a Gmail or a Google Apps ones.
     *
     * @param domain domain to check
     * @return true if our account is a Gmail or a Google Apps ones.
     */
    public static boolean isGmailOrGoogleAppsAccount(String domain)
    {
        SRVRecord srvRecords[] = null;

        try
        {
            srvRecords = NetworkUtils.getSRVRecords("xmpp-client", "tcp",
                domain);
        }
        catch (ParseException e)
        {
            logger.info("Failed to get SRV records for XMPP domain");
            return false;
        }
        catch (DnssecException e)
        {
            logger.error("DNSSEC failure while checking for google domains", e);
            return false;
        }

        if(srvRecords == null)
        {
            return false;
        }

        for(SRVRecord srv : srvRecords)
        {
            if(srv.getTarget().endsWith("google.com") ||
                    srv.getTarget().endsWith("google.com."))
            {
                return true;
            }
        }

        return false;
    }

    /**
     * Sets the traffic class for the XMPP signalling socket.
     */
    private void setTrafficClass()
    {
        Socket s = getSocket();

        if(s != null)
        {
            ConfigurationService configService =
                JabberActivator.getConfigurationService();
            String dscp = configService.getString(XMPP_DSCP_PROPERTY);

            if(dscp != null)
            {
                try
                {
                    int dscpInt = Integer.parseInt(dscp) << 2;

                    if(dscpInt > 0)
                        s.setTrafficClass(dscpInt);
                }
                catch (Exception e)
                {
                    logger.info("Failed to set trafficClass", e);
                }
            }
        }
    }

    /**
     * Gets the entity ID of the first Jitsi Videobridge associated with
     * {@link #connection} i.e. provided by the <tt>serviceName</tt> of
     * <tt>connection</tt>.
     *
     * @return the entity ID of the first Jitsi Videobridge associated with
     * <tt>connection</tt>
     */
    public String getJitsiVideobridge()
    {
        Connection connection = getConnection();

        if (connection != null)
        {
            ScServiceDiscoveryManager discoveryManager = getDiscoveryManager();
            String serviceName = connection.getServiceName();
            DiscoverItems discoverItems = null;

            try
            {
                discoverItems = discoveryManager.discoverItems(serviceName);
            }
            catch (XMPPException xmppe)
            {
                if (logger.isDebugEnabled())
                {
                    logger.debug(
                            "Failed to discover the items associated with"
                                + " Jabber entity: " + serviceName,
                            xmppe);
                }
            }
            if (discoverItems != null)
            {
                Iterator<DiscoverItems.Item> discoverItemIter
                    = discoverItems.getItems();

                while (discoverItemIter.hasNext())
                {
                    DiscoverItems.Item discoverItem = discoverItemIter.next();
                    String entityID = discoverItem.getEntityID();
                    DiscoverInfo discoverInfo = null;

                    try
                    {
                        discoverInfo = discoveryManager.discoverInfo(entityID);
                    }
                    catch (XMPPException xmppe)
                    {
                        logger.warn(
                                "Failed to discover information about Jabber"
                                    + " entity: " + entityID,
                                xmppe);
                    }
                    if ((discoverInfo != null)
                            && discoverInfo.containsFeature(
                                    ColibriConferenceIQ.NAMESPACE))
                    {
                        return entityID;
                    }
                }
            }
        }

        return null;
    }

    /**
     * Load jabber service class, their static context will register
     * what is needed. Used in android as when using the other jars
     * these services are loaded from the jar manifest.
     */
    private static void loadJabberServiceClasses()
    {
        if(!OSUtils.IS_ANDROID)
            return;

        try
        {
            // pre-configure smack in android
            // just to load class to init their static blocks
            SmackConfiguration.getVersion();
            Class.forName(ServiceDiscoveryManager.class.getName());

            Class.forName(DelayInformation.class.getName());
            Class.forName(org.jivesoftware.smackx
                .provider.DelayInformationProvider.class.getName());
            Class.forName(org.jivesoftware.smackx
                .bytestreams.socks5.Socks5BytestreamManager.class.getName());
            Class.forName(XHTMLManager.class.getName());
            Class.forName(org.jivesoftware.smackx
                .bytestreams.ibb.InBandBytestreamManager.class.getName());

        }
        catch(ClassNotFoundException e)
        {
            logger.error("Error loading classes in smack", e);
        }
    }

    /**
     * Obtains XMPP connection's socket.
     * @return <tt>Socket</tt> instance used by the underlying XMPP connection
     * or <tt>null</tt> if "non socket" type of transport is currently used.
     */
    private Socket getSocket()
    {
        return connection != null ? connection.getSocket() : null;
    }

    /**
     * Return the SSL socket (if TLS used).
     * @return The SSL socket or null if not used
     */
    SSLSocket getSSLSocket()
    {
        final Socket socket = getSocket();

        return (socket instanceof SSLSocket) ? (SSLSocket) socket : null;
    }
}
