/*
This file is part of Project MAXS.
MAXS and its modules is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
MAXS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with MAXS. If not, see <http://www.gnu.org/licenses/>.
*/
package org.projectmaxs.transport.xmpp.xmppservice;
import java.io.File;
import java.io.Reader;
import java.io.Writer;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import org.jivesoftware.smack.SmackConfiguration;
import org.jivesoftware.smack.SmackException.ConnectionException;
import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPConnection;
import org.jivesoftware.smack.debugger.JulDebugger;
import org.jivesoftware.smack.debugger.SmackDebugger;
import org.jivesoftware.smack.debugger.SmackDebuggerFactory;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterLoadedListener;
import org.jivesoftware.smack.roster.rosterstore.DirectoryRosterStore;
import org.jivesoftware.smack.roster.rosterstore.RosterStore;
import org.jivesoftware.smack.sasl.SASLMechanism;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jivesoftware.smack.util.DNSUtil;
import org.jivesoftware.smack.util.StringTransformer;
import org.jivesoftware.smack.util.dns.HostAddress;
import org.jivesoftware.smackx.address.MultipleRecipientManager;
import org.jivesoftware.smackx.carbons.packet.CarbonExtension;
import org.jivesoftware.smackx.disco.ServiceDiscoveryManager;
import org.jivesoftware.smackx.disco.packet.DiscoverInfo;
import org.jivesoftware.smackx.iqlast.LastActivityManager;
import org.jivesoftware.smackx.ping.PingManager;
import org.jivesoftware.smackx.xhtmlim.XHTMLManager;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.EntityFullJid;
import org.jxmpp.jid.EntityJid;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import org.jxmpp.stringprep.XmppStringprepException;
import org.projectmaxs.shared.global.GlobalConstants;
import org.projectmaxs.shared.global.util.FileUtil;
import org.projectmaxs.shared.global.util.Log;
import org.projectmaxs.shared.maintransport.CommandOrigin;
import org.projectmaxs.shared.maintransport.TransportConstants;
import org.projectmaxs.shared.transport.transform.TransformMessageContent;
import org.projectmaxs.transport.xmpp.Settings;
import org.projectmaxs.transport.xmpp.database.MessagesTable;
import org.projectmaxs.transport.xmpp.smack.provider.MAXSElementProvider;
import org.projectmaxs.transport.xmpp.smack.stanza.MAXSElement;
import org.projectmaxs.transport.xmpp.util.ConnectivityManagerUtil;
import org.projectmaxs.transport.xmpp.util.Constants;
import org.projectmaxs.transport.xmpp.util.XHTMLIMUtil;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
public class XMPPService {
private static final Log LOG = Log.getLog();
private static XMPPService sXMPPService;
private final Set<StateChangeListener> mStateChangeListeners = new CopyOnWriteArraySet<StateChangeListener>();
private final Settings mSettings;
private final MessagesTable mMessagesTable;
private final Context mContext;
private final HandleTransportStatus mHandleTransportStatus;
private XMPPStatus mXMPPStatus;
private State mState = State.Disconnected;
static {
ServiceDiscoveryManager.setDefaultIdentity(
new DiscoverInfo.Identity("client", GlobalConstants.HUMAN_READABLE_NAME, "bot"));
// TODO This is not really needed, but for some reason the static initializer block of
// LastActivityManager is not run. This could be a problem caused by aSmack together with
// dalvik, as the initializer is run on Smack's test cases.
LastActivityManager.setEnabledPerDefault(true);
// Some network types, especially GPRS or EDGE is rural areas have a very slow response
// time. Smack's default packet reply timeout of 5 seconds is way to low for such networks,
// so we increase it to 2 minutes.
// This value must also be greater then the highest returned bundle and defer value.
SmackConfiguration.setDefaultPacketReplyTimeout(2 * 60 * 1000);
SmackConfiguration.addDisabledSmackClass("org.jivesoftware.smackx.hoxt.HOXTManager");
SmackConfiguration.addDisabledSmackClass("org.jivesoftware.smack.ReconnectionManager");
SmackConfiguration
.addDisabledSmackClass("org.jivesoftware.smackx.muc.MultiUserChatManager");
SmackConfiguration.addDisabledSmackClass("org.jivesoftware.smackx.json");
SmackConfiguration.addDisabledSmackClass("org.jivesoftware.smackx.gcm");
SmackConfiguration.addDisabledSmackClass("org.jivesoftware.smackx.xdata.XDataManager");
SmackConfiguration
.addDisabledSmackClass("org.jivesoftware.smackx.xdatalayout.XDataLayoutManager");
SmackConfiguration.addDisabledSmackClass(
"org.jivesoftware.smackx.xdatavalidation.XDataValidationManager");
SmackConfiguration.addDisabledSmackClass("org.jivesoftware.smackx.iot");
SmackConfiguration.addDisabledSmackClass("org.jivesoftares.smack.legacy");
SmackConfiguration.addDisabledSmackClass("org.jivesoftware.smack.java7");
// @formatter:off
SmackConfiguration.addDisabledSmackClasses(
"org.jivesoftware.smack.util.dns.javax",
"org.jivesoftware.smack.util.dns.dnsjava",
"org.jivesoftware.smack.sasl.javax",
"org.jivesoftware.smack.legacy");
// @formatter:on
DNSUtil.setIdnaTransformer(new StringTransformer() {
@Override
public String transform(String string) {
return java.net.IDN.toASCII(string);
}
});
SASLMechanism.setSaslPrepTransformer(new StringTransformer() {
@Override
public String transform(String string) {
return Normalizer.normalize(string, Form.NFKC);
}
});
SmackConfiguration.setDebuggerFactory(new SmackDebuggerFactory() {
@Override
public SmackDebugger create(XMPPConnection connection, Writer writer, Reader reader) {
return new JulDebugger(connection, writer, reader);
}
});
MAXSElementProvider.setup();
}
private final Runnable mReconnectRunnable = new Runnable() {
@Override
public void run() {
LOG.d("scheduleReconnect: calling tryToConnect");
tryToConnect();
}
};
/**
* Switch boolean to ensure that the disconnected(XMPPConnection) listeners are
* only run if there was a previous connected connection.
*/
private boolean mConnected = false;
private XMPPTCPConnectionConfiguration mConnectionConfiguration;
private XMPPTCPConnection mConnection;
private Handler mReconnectHandler;
private int mReconnectionAttemptCount;
/**
* Get an XMPPService
*
* Note that because of MemorizingTrustManager Context must be an instance of Application,
* Service or Activity. Therefore if you have an Context which is not Service or Activity, use
* getApplication().
*
* @param context
* as an instance of Application, Service or Activity.
* @return The XMPPService instance.
*/
public static synchronized XMPPService getInstance(Context context) {
if (sXMPPService == null) sXMPPService = new XMPPService(context);
return sXMPPService;
}
private XMPPService(Context context) {
XMPPVersion.initialize(context);
XMPPBundleAndDefer.initialize(context);
mContext = context;
mSettings = Settings.getInstance(context);
mMessagesTable = MessagesTable.getInstance(context);
// SendStanzaDatabaseHandler should be the first
addListener(new SendStanzaDatabaseHandler(this));
addListener(new HandleChatPacketListener(this));
addListener(new HandleConnectionListener(this));
addListener(new HandleMessagesListener(this));
addListener(new XMPPPingManager(this));
addListener(new XMPPFileTransfer(context));
addListener(new XMPPPrivacyList(mSettings));
mHandleTransportStatus = new HandleTransportStatus(context);
addListener(mHandleTransportStatus);
XMPPRoster xmppRoster = new XMPPRoster(mSettings);
addListener(xmppRoster);
mXMPPStatus = new XMPPStatus(xmppRoster, context);
addListener(mXMPPStatus);
}
public static enum State {
Connected, Connecting, Disconnecting, Disconnected, InstantDisconnected, WaitingForNetwork, WaitingForRetry;
}
public State getCurrentState() {
return mState;
}
public boolean isConnected() {
return (getCurrentState() == State.Connected);
}
public HandleTransportStatus getHandleTransportStatus() {
return mHandleTransportStatus;
}
public void addListener(StateChangeListener listener) {
mStateChangeListeners.add(listener);
}
public void removeListener(StateChangeListener listener) {
synchronized (mStateChangeListeners) {
mStateChangeListeners.remove(listener);
}
}
public void connect() {
changeState(XMPPService.State.Connected);
}
public void disconnect() {
changeState(XMPPService.State.Disconnected);
}
public void instantDisconnect() {
changeState(XMPPService.State.InstantDisconnected);
}
public void reconnect() {
disconnect();
connect();
}
public void setStatus(String status) {
mXMPPStatus.setStatus(status);
}
public void networkDisconnected() {
changeState(State.WaitingForNetwork);
}
public void send(Jid to, String body) {
switch (mState) {
case Disconnected:
case Disconnecting:
LOG.w("Transport is disconnected, not going to send message to " + to);
return;
default:
break;
}
if (!shouldUseXmppConnection()) {
LOG.w("Connection is not connected and no resumption possible, not going to send message to "
+ to);
return;
}
Message message = new Message();
message.setTo(to);
message.setBody(body);
try {
mConnection.sendStanza(message);
} catch (InterruptedException | NotConnectedException e) {
LOG.w("send", e);
}
}
public void send(org.projectmaxs.shared.global.Message message, CommandOrigin origin) {
// If the origin is null, then we are receiving a broadcast message from
// main. TODO document that origin can be null
if (origin == null) {
sendAsMessage(message, null, null);
return;
}
String action = origin.getIntentAction();
String originId = origin.getOriginId();
String originIssuerInfo = origin.getOriginIssuerInfo();
if (Constants.ACTION_SEND_AS_MESSAGE.equals(action)) {
sendAsMessage(message, originIssuerInfo, originId);
} else if (Constants.ACTION_SEND_AS_IQ.equals(action)) {
sendAsIQ(message, originIssuerInfo, originId);
} else {
throw new IllegalStateException("XMPPService send: unknown action=" + action);
}
}
public XMPPConnection getConnection() {
return mConnection;
}
public boolean fastPingServer() {
if (mConnection == null) return false;
PingManager pingManager = PingManager.getInstanceFor(mConnection);
XMPPBundleAndDefer.disableBundleAndDefer();
try {
return pingManager.pingMyServer(false, 1500);
} catch (InterruptedException | NotConnectedException e) {
return false;
} finally {
XMPPBundleAndDefer.enableBundleAndDefer();
}
}
Context getContext() {
return mContext;
}
private void sendAsMessage(org.projectmaxs.shared.global.Message message,
String originIssuerInfo, String originId) {
if (!shouldUseXmppConnection()) {
// TODO I think that this could for example happen when the service
// is not started but e.g. the SMS receiver get's a new message.
LOG.i("sendAsMessage: Not connected, adding message to DB. mConnection=" + mConnection);
mMessagesTable.addMessage(message, Constants.ACTION_SEND_AS_MESSAGE, originIssuerInfo,
originId);
return;
}
Message packet = new Message();
packet.setType(Message.Type.chat);
packet.setBody(TransformMessageContent.toString(message));
packet.setThread(originId);
// Add a private carbon extension so that this message wont get carbon copied. MAXS does
// already send the message to all resources. If a recipient has carbons enabled and we
// wouldn't add the private element, then he would receive the message multiple times.
CarbonExtension.Private.addTo(packet);
// Add a MAXS element. MAXS itself will ignore messages with a MAXS element in order to
// prevent endless loops of message sending between one or multiple MAXS instances.
MAXSElement.addTo(packet);
List<EntityJid> toList = new LinkedList<>();
// No 'originIssueInfo (which is the to JID in this case) specified. The message is typical
// a notification, so we are going to broadcast it to all master JIDs.
if (originIssuerInfo == null) {
Set<BareJid> jidsWithExcludedResources = new HashSet<BareJid>();
Roster roster = Roster.getInstanceFor(mConnection);
// Broadcast to all masterJID resources
for (BareJid masterJid : mSettings.getMasterJids()) {
Collection<Presence> presences = roster.getAvailablePresences(masterJid);
for (Presence p : presences) {
Jid jid = p.getFrom();
EntityFullJid fullJID = jid.asEntityFullJidIfPossible();
if (fullJID == null) {
LOG.e("Could not convert '" + jid + "' to full JID");
continue;
}
if (!mSettings.isExcludedResource(fullJID.getResourcepart())) {
toList.add(fullJID);
} else {
jidsWithExcludedResources.add(fullJID.asBareJid());
}
}
}
// Broadcast to all offline masterJIDs
for (EntityBareJid masterJid : mSettings.getMasterJids()) {
boolean found = false;
for (EntityJid toJid : toList) {
if (toJid.asBareJid().equals(masterJid)) {
found = true;
break;
}
}
// Maybe add this master JID, if it isn't already contained in toList
if (!found) {
if (jidsWithExcludedResources.contains(masterJid)
&& roster.getPresences(masterJid).size() == 1) {
// Do not send a message to this JID if it would get received by an excluded
// resource, ie. when the excluded resource is the only online presence.
continue;
}
toList.add(masterJid);
}
}
}
// A JID was specified as receiver. This are typical replies to a command send by the
// receiver. This is not a notification, do not broadcast.
else {
EntityFullJid to;
try {
to = JidCreate.entityFullFrom(originIssuerInfo);
} catch (XmppStringprepException e) {
LOG.e("Could not convert originIssueInfo to full JID", e);
return;
}
toList.add(to);
}
boolean atLeastOneSupportsXHTMLIM = false;
for (EntityJid jid : toList) {
if (!jid.hasResource()) {
continue;
}
try {
atLeastOneSupportsXHTMLIM = XHTMLManager.isServiceEnabled(mConnection, jid);
} catch (Exception e) {
atLeastOneSupportsXHTMLIM = false;
}
if (atLeastOneSupportsXHTMLIM) break;
}
if (atLeastOneSupportsXHTMLIM)
XHTMLIMUtil.addXHTMLIM(packet, TransformMessageContent.toFormatedText(message));
try {
MultipleRecipientManager.send(mConnection, packet, toList, null, null);
} catch (Exception e) {
LOG.e("sendAsMessage: Got Exception, adding message to DB", e);
mMessagesTable.addMessage(message, Constants.ACTION_SEND_AS_MESSAGE, originIssuerInfo,
originId);
}
// Stop the current bundleAndDefer *after* the message has been sent.
XMPPBundleAndDefer.stopCurrentBundleAndDefer();
}
private void sendAsIQ(org.projectmaxs.shared.global.Message message, String originIssuerInfo,
String issuerId) {
// in a not so far future
}
protected void newMessageFromMasterJID(Message message) {
String command = message.getBody();
if (command == null) {
LOG.e("newMessageFromMasterJID: empty body");
return;
}
// Trim the command to remove extra whitespace, which e.g. could be send by clients trying
// to negotiate OTR. References:
// - https://github.com/python-otr/gajim-otr/issues/9
// - https://trac-plugins.gajim.org/ticket/97
command = command.trim();
String issuerInfo = message.getFrom().toString();
LOG.d("newMessageFromMasterJID: command=" + command + " from=" + issuerInfo);
Intent intent = new Intent(GlobalConstants.ACTION_PERFORM_COMMAND);
CommandOrigin origin = new CommandOrigin(Constants.PACKAGE,
Constants.ACTION_SEND_AS_MESSAGE, issuerInfo, null);
intent.putExtra(TransportConstants.EXTRA_COMMAND, command);
intent.putExtra(TransportConstants.EXTRA_COMMAND_ORIGIN, origin);
intent.setClassName(GlobalConstants.MAIN_PACKAGE,
TransportConstants.MAIN_TRANSPORT_SERVICE);
ComponentName cn = mContext.startService(intent);
if (cn == null) {
LOG.e("newMessageFromMasterJID: could not start main transport service");
}
}
private void scheduleReconnect(String optionalReason) {
if (mReconnectHandler == null) mReconnectHandler = new Handler();
newState(State.WaitingForRetry, optionalReason);
mReconnectHandler.removeCallbacks(mReconnectRunnable);
int reconnectDelaySeconds;
final int MINIMAL_DELAY_SECONDS = 10;
final int ATTEMPTS_WITHOUT_PENALTY = 60;
if (mReconnectionAttemptCount <= ATTEMPTS_WITHOUT_PENALTY) {
reconnectDelaySeconds = MINIMAL_DELAY_SECONDS;
} else {
int delayFunctionResult = MINIMAL_DELAY_SECONDS * ((int) Math
.pow(mReconnectionAttemptCount - ATTEMPTS_WITHOUT_PENALTY - 1, 1.2));
// Maximum delay is 30 minutes
reconnectDelaySeconds = Math.max(delayFunctionResult, 60 * 30);
}
mReconnectionAttemptCount++;
LOG.d("scheduleReconnect: scheduling reconnect in " + reconnectDelaySeconds + " seconds");
mReconnectHandler.postDelayed(mReconnectRunnable, reconnectDelaySeconds * 1000);
}
private void newState(State newState) {
newState(newState, "");
}
/**
* Notifies the StateChangeListeners about the new state and sets mState to
* newState. Does not add a log message.
*
* @param newState
* @param reason
* the optional reason for the new state
*/
private void newState(State newState, String reason) {
if (reason == null) reason = "";
synchronized (mStateChangeListeners) {
switch (newState) {
case Connected:
for (StateChangeListener l : mStateChangeListeners) {
try {
l.connected(mConnection);
} catch (NotConnectedException e) {
LOG.w("newState", e);
// Do not call 'changeState(State.Disconnected)' here, instead simply
// schedule reconnect since we obviously didn't reach the connected state.
// Changing the state to Disconnected will create a transition from
// 'Connecting' to 'Disconnected', which why avoid implementing here
scheduleReconnect("Disconnected while connecting");
return;
}
}
mConnected = true;
break;
case InstantDisconnected:
case Disconnected:
for (StateChangeListener l : mStateChangeListeners) {
l.disconnected(reason);
if (mConnection != null && mConnected) l.disconnected(mConnection);
}
mConnected = false;
break;
case Connecting:
for (StateChangeListener l : mStateChangeListeners)
l.connecting();
break;
case Disconnecting:
for (StateChangeListener l : mStateChangeListeners)
l.disconnecting();
break;
case WaitingForNetwork:
for (StateChangeListener l : mStateChangeListeners)
l.waitingForNetwork();
break;
case WaitingForRetry:
for (StateChangeListener l : mStateChangeListeners)
l.waitingForRetry(reason);
break;
default:
break;
}
}
mState = newState;
}
private synchronized void changeState(State desiredState) {
LOG.d("changeState: mState=" + mState + ", desiredState=" + desiredState);
switch (mState) {
case Connected:
switch (desiredState) {
case Connected:
break;
case Disconnected:
disconnectConnection(false);
break;
case InstantDisconnected:
case WaitingForNetwork:
disconnectConnection(true);
newState(desiredState);
break;
default:
throw new IllegalStateException();
}
break;
case InstantDisconnected:
case Disconnected:
switch (desiredState) {
case InstantDisconnected:
case Disconnected:
break;
case Connected:
tryToConnect();
break;
case WaitingForNetwork:
newState(State.WaitingForNetwork);
break;
default:
throw new IllegalStateException();
}
break;
case WaitingForNetwork:
switch (desiredState) {
case WaitingForNetwork:
break;
case Connected:
tryToConnect();
break;
case InstantDisconnected:
case Disconnected:
newState(desiredState);
break;
default:
throw new IllegalStateException();
}
break;
case WaitingForRetry:
switch (desiredState) {
case WaitingForNetwork:
newState(State.WaitingForNetwork);
break;
case Connected:
// Do nothing here, instead, wait until the reconnect runnable did it's job.
// Otherwise deadlocks may occur, because the connection attempts will block the
// main thread, which will prevent SmackAndroid from receiving the
// ConnecvitvityChange receiver and calling Resolver.refresh(). So we have no
// up-to-date DNS server information, which will cause connect to fail.
break;
case InstantDisconnected:
case Disconnected:
newState(desiredState);
mReconnectHandler.removeCallbacks(mReconnectRunnable);
break;
default:
throw new IllegalStateException();
}
break;
default:
throw new IllegalStateException("changeState: Unknown state change combination. mState="
+ mState + ", desiredState=" + desiredState);
}
}
private synchronized void tryToConnect() {
String failureReason = mSettings.checkIfReadyToConnect();
if (failureReason != null) {
LOG.w("tryToConnect: failureReason=" + failureReason);
mHandleTransportStatus.setAndSendStatus("Unable to connect: " + failureReason);
return;
}
if (isConnected()) {
LOG.d("tryToConnect: already connected, nothing to do here");
return;
}
if (!ConnectivityManagerUtil.hasDataConnection(mContext)) {
LOG.d("tryToConnect: no data connection available");
newState(State.WaitingForNetwork);
return;
}
newState(State.Connecting);
XMPPTCPConnection connection;
boolean newConnection = false;
// We need to use an Application context instance here, because some Contexts may not work.
XMPPTCPConnectionConfiguration latestConnectionConfiguration;
try {
latestConnectionConfiguration = mSettings.getConnectionConfiguration(mContext);
} catch (XmppStringprepException e) {
LOG.e("tryToConnect: getConnectionConfiguration failed. New State: Disconnected", e);
newState(State.Disconnected, e.getLocalizedMessage());
return;
}
if (mConnection == null || mConnectionConfiguration != latestConnectionConfiguration) {
mConnectionConfiguration = latestConnectionConfiguration;
connection = new XMPPTCPConnection(mConnectionConfiguration);
final Roster roster = Roster.getInstanceFor(connection);
// Setup the roster store
File rosterStoreDirectory = FileUtil.getFileDir(mContext, "rosterStore");
RosterStore rosterStore = DirectoryRosterStore.init(rosterStoreDirectory);
roster.setRosterStore(rosterStore);
roster.addRosterLoadedListener(new RosterLoadedListener() {
@Override
public void onRosterLoaded(Roster roster) {
LOG.d("RosterLoadedListener.onRosterLoaded() invoked, roster has been loaded");
}
@Override
public void onRosterLoadingFailed(Exception exception) {
LOG.w("Failed to load roster", exception);
}
});
newConnection = true;
} else {
connection = mConnection;
}
// Stream Management (XEP-198)
connection.setUseStreamManagement(mSettings.isStreamManagementEnabled());
// Again a value that's hard to get right. Right now, we try it with 5 minutes, as Stream
// resumption is meant for situations where the network switches, not when the Android
// system kills and later restarts the service (in which case, SM resumption would be not
// possible anyways).
connection.setPreferredResumptionTime(5 * 60); // 5 minutes
LOG.d("tryToConnect: connect");
XMPPBundleAndDefer.disableBundleAndDefer();
try {
connection.connect();
} catch (Exception e) {
XMPPBundleAndDefer.enableBundleAndDefer();
LOG.e("tryToConnect: Exception from connect()", e);
if (e instanceof ConnectionException) {
ConnectionException ce = (ConnectionException) e;
String error = "The following host's failed to connect to:";
for (HostAddress ha : ce.getFailedAddresses())
error += " " + ha;
LOG.d("tryToConnect: " + error);
}
scheduleReconnect(e.getLocalizedMessage());
return;
}
try {
connection.login();
} catch (NoResponseException e) {
LOG.w("tryToConnect: NoResponseException. Scheduling reconnect.");
scheduleReconnect("Not response while loggin in.");
return;
} catch (Exception e) {
LOG.e("tryToConnect: login failed. New State: Disconnected", e);
newState(State.Disconnected, e.getLocalizedMessage());
return;
} finally {
XMPPBundleAndDefer.enableBundleAndDefer();
}
// Login Successful
mConnection = connection;
if (newConnection) {
synchronized (mStateChangeListeners) {
for (StateChangeListener l : mStateChangeListeners) {
l.newConnection(mConnection);
}
}
}
mReconnectionAttemptCount = 0;
newState(State.Connected);
LOG.d("tryToConnect: successfully connected \\o/");
}
private synchronized void disconnectConnection(boolean instant) {
if (mConnection != null) {
if (mConnection.isConnected()) {
newState(State.Disconnecting);
LOG.d("disconnectConnection: disconnect start. instant=" + instant);
if (instant) {
mConnection.instantShutdown();
} else {
mConnection.disconnect();
}
LOG.d("disconnectConnection: disconnect stop");
}
newState(State.Disconnected);
}
}
private boolean shouldUseXmppConnection() {
if (mConnection == null) {
return false;
}
if (!mConnection.isAuthenticated()) {
return false;
}
// If it is not connected and not resumable, return false
if (!mConnection.isConnected() && !mConnection.isDisconnectedButSmResumptionPossible()) {
return false;
}
return true;
}
}