XmppConnection.java

   1package eu.siacs.conversations.xmpp;
   2
   3import android.content.Context;
   4import android.content.SharedPreferences;
   5import android.os.Bundle;
   6import android.os.Parcelable;
   7import android.os.PowerManager;
   8import android.os.PowerManager.WakeLock;
   9import android.os.SystemClock;
  10import android.preference.PreferenceManager;
  11import android.util.Log;
  12import android.util.SparseArray;
  13
  14import org.apache.http.conn.ssl.StrictHostnameVerifier;
  15import org.xmlpull.v1.XmlPullParserException;
  16
  17import java.io.IOException;
  18import java.io.InputStream;
  19import java.io.OutputStream;
  20import java.math.BigInteger;
  21import java.net.IDN;
  22import java.net.InetAddress;
  23import java.net.InetSocketAddress;
  24import java.net.Socket;
  25import java.net.UnknownHostException;
  26import java.security.KeyManagementException;
  27import java.security.NoSuchAlgorithmException;
  28import java.util.ArrayList;
  29import java.util.Arrays;
  30import java.util.HashMap;
  31import java.util.Hashtable;
  32import java.util.LinkedList;
  33import java.util.List;
  34import java.util.Map.Entry;
  35
  36import javax.net.ssl.HostnameVerifier;
  37import javax.net.ssl.SSLContext;
  38import javax.net.ssl.SSLSocket;
  39import javax.net.ssl.SSLSocketFactory;
  40import javax.net.ssl.X509TrustManager;
  41
  42import eu.siacs.conversations.Config;
  43import eu.siacs.conversations.entities.Account;
  44import eu.siacs.conversations.services.XmppConnectionService;
  45import eu.siacs.conversations.utils.CryptoHelper;
  46import eu.siacs.conversations.utils.DNSHelper;
  47import eu.siacs.conversations.utils.zlib.ZLibInputStream;
  48import eu.siacs.conversations.utils.zlib.ZLibOutputStream;
  49import eu.siacs.conversations.xml.Element;
  50import eu.siacs.conversations.xml.Tag;
  51import eu.siacs.conversations.xml.TagWriter;
  52import eu.siacs.conversations.xml.XmlReader;
  53import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
  54import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
  55import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
  56import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  57import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
  58import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
  59import eu.siacs.conversations.xmpp.stanzas.csi.ActivePacket;
  60import eu.siacs.conversations.xmpp.stanzas.csi.InactivePacket;
  61import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
  62import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
  63import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
  64import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
  65
  66public class XmppConnection implements Runnable {
  67
  68	private static final int PACKET_IQ = 0;
  69	private static final int PACKET_MESSAGE = 1;
  70	private static final int PACKET_PRESENCE = 2;
  71	private final Context applicationContext;
  72	protected Account account;
  73	private WakeLock wakeLock;
  74	private Socket socket;
  75	private XmlReader tagReader;
  76	private TagWriter tagWriter;
  77	private Features features = new Features(this);
  78	private boolean shouldBind = true;
  79	private boolean shouldAuthenticate = true;
  80	private Element streamFeatures;
  81	private HashMap<String, List<String>> disco = new HashMap<String, List<String>>();
  82	private String streamId = null;
  83	private int smVersion = 3;
  84	private SparseArray<String> messageReceipts = new SparseArray<String>();
  85	private boolean usingCompression = false;
  86	private boolean usingEncryption = false;
  87	private int stanzasReceived = 0;
  88	private int stanzasSent = 0;
  89	private long lastPaketReceived = 0;
  90	private long lastPingSent = 0;
  91	private long lastConnect = 0;
  92	private long lastSessionStarted = 0;
  93	private int attempt = 0;
  94	private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
  95	private OnPresencePacketReceived presenceListener = null;
  96	private OnJinglePacketReceived jingleListener = null;
  97	private OnIqPacketReceived unregisteredIqListener = null;
  98	private OnMessagePacketReceived messageListener = null;
  99	private OnStatusChanged statusListener = null;
 100	private OnBindListener bindListener = null;
 101	private OnMessageAcknowledged acknowledgedListener = null;
 102	private XmppConnectionService mXmppConnectionService = null;
 103
 104	public XmppConnection(Account account, XmppConnectionService service) {
 105		this.account = account;
 106		this.wakeLock = service.getPowerManager().newWakeLock(
 107				PowerManager.PARTIAL_WAKE_LOCK, account.getJid());
 108		tagWriter = new TagWriter();
 109		mXmppConnectionService = service;
 110		applicationContext = service.getApplicationContext();
 111	}
 112
 113	protected void changeStatus(int nextStatus) {
 114		if (account.getStatus() != nextStatus) {
 115			if ((nextStatus == Account.STATUS_OFFLINE)
 116					&& (account.getStatus() != Account.STATUS_CONNECTING)
 117					&& (account.getStatus() != Account.STATUS_ONLINE)
 118					&& (account.getStatus() != Account.STATUS_DISABLED)) {
 119				return;
 120			}
 121			if (nextStatus == Account.STATUS_ONLINE) {
 122				this.attempt = 0;
 123			}
 124			account.setStatus(nextStatus);
 125			if (statusListener != null) {
 126				statusListener.onStatusChanged(account);
 127			}
 128		}
 129	}
 130
 131	protected void connect() {
 132		Log.d(Config.LOGTAG, account.getJid() + ": connecting");
 133		usingCompression = false;
 134		usingEncryption = false;
 135		lastConnect = SystemClock.elapsedRealtime();
 136		lastPingSent = SystemClock.elapsedRealtime();
 137		this.attempt++;
 138		try {
 139			shouldAuthenticate = shouldBind = !account
 140					.isOptionSet(Account.OPTION_REGISTER);
 141			tagReader = new XmlReader(wakeLock);
 142			tagWriter = new TagWriter();
 143			packetCallbacks.clear();
 144			this.changeStatus(Account.STATUS_CONNECTING);
 145			Bundle result = DNSHelper.getSRVRecord(account.getServer());
 146			ArrayList<Parcelable> values = result.getParcelableArrayList("values");
 147			if ("timeout".equals(result.getString("error"))) {
 148				Log.d(Config.LOGTAG, account.getJid() + ": dns timeout");
 149				this.changeStatus(Account.STATUS_OFFLINE);
 150				return;
 151			} else if (values != null) {
 152				int i = 0;
 153				boolean socketError = true;
 154				while (socketError && values.size() > i) {
 155					Bundle namePort = (Bundle) values.get(i);
 156					try {
 157						String srvRecordServer = namePort.getString("name");
 158						int srvRecordPort = namePort.getInt("port");
 159						String srvIpServer = namePort.getString("ipv4");
 160						InetSocketAddress addr;
 161						if (srvIpServer != null) {
 162							addr = new InetSocketAddress(srvIpServer, srvRecordPort);
 163							Log.d(Config.LOGTAG, account.getJid()
 164									+ ": using values from dns " + srvRecordServer
 165									+ "[" + srvIpServer + "]:" + srvRecordPort);
 166						} else {
 167							addr = new InetSocketAddress(srvRecordServer, srvRecordPort);
 168							Log.d(Config.LOGTAG, account.getJid()
 169									+ ": using values from dns "
 170									+ srvRecordServer + ":" + srvRecordPort);
 171						}
 172						socket = new Socket();
 173						socket.connect(addr, 20000);
 174						socketError = false;
 175					} catch (UnknownHostException e) {
 176						Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage());
 177						i++;
 178					} catch (IOException e) {
 179						Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage());
 180						i++;
 181					}
 182				}
 183				if (socketError) {
 184					this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
 185					if (wakeLock.isHeld()) {
 186						try {
 187							wakeLock.release();
 188						} catch (RuntimeException re) {
 189						}
 190					}
 191					return;
 192				}
 193			} else if (result.containsKey("error")
 194					&& "nosrv".equals(result.getString("error", null))) {
 195				socket = new Socket(account.getServer(), 5222);
 196			} else {
 197				Log.d(Config.LOGTAG, account.getJid()
 198						+ ": timeout in DNS resolution");
 199				changeStatus(Account.STATUS_OFFLINE);
 200				return;
 201			}
 202			OutputStream out = socket.getOutputStream();
 203			tagWriter.setOutputStream(out);
 204			InputStream in = socket.getInputStream();
 205			tagReader.setInputStream(in);
 206			tagWriter.beginDocument();
 207			sendStartStream();
 208			Tag nextTag;
 209			while ((nextTag = tagReader.readTag()) != null) {
 210				if (nextTag.isStart("stream")) {
 211					processStream(nextTag);
 212					break;
 213				} else {
 214					Log.d(Config.LOGTAG,
 215							"found unexpected tag: " + nextTag.getName());
 216					return;
 217				}
 218			}
 219			if (socket.isConnected()) {
 220				socket.close();
 221			}
 222		} catch (UnknownHostException e) {
 223			this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
 224			if (wakeLock.isHeld()) {
 225				try {
 226					wakeLock.release();
 227				} catch (RuntimeException re) {
 228				}
 229			}
 230			return;
 231		} catch (IOException e) {
 232			Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage());
 233			this.changeStatus(Account.STATUS_OFFLINE);
 234			if (wakeLock.isHeld()) {
 235				try {
 236					wakeLock.release();
 237				} catch (RuntimeException re) {
 238				}
 239			}
 240			return;
 241		} catch (NoSuchAlgorithmException e) {
 242			Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage());
 243			this.changeStatus(Account.STATUS_OFFLINE);
 244			Log.d(Config.LOGTAG, "compression exception " + e.getMessage());
 245			if (wakeLock.isHeld()) {
 246				try {
 247					wakeLock.release();
 248				} catch (RuntimeException re) {
 249				}
 250			}
 251			return;
 252		} catch (XmlPullParserException e) {
 253			Log.d(Config.LOGTAG, account.getJid() + ": " + e.getMessage());
 254			this.changeStatus(Account.STATUS_OFFLINE);
 255			if (wakeLock.isHeld()) {
 256				try {
 257					wakeLock.release();
 258				} catch (RuntimeException re) {
 259				}
 260			}
 261			return;
 262		}
 263
 264	}
 265
 266	@Override
 267	public void run() {
 268		connect();
 269	}
 270
 271	private void processStream(Tag currentTag) throws XmlPullParserException,
 272			IOException, NoSuchAlgorithmException {
 273		Tag nextTag = tagReader.readTag();
 274		while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
 275			if (nextTag.isStart("error")) {
 276				processStreamError(nextTag);
 277			} else if (nextTag.isStart("features")) {
 278				processStreamFeatures(nextTag);
 279			} else if (nextTag.isStart("proceed")) {
 280				switchOverToTls(nextTag);
 281			} else if (nextTag.isStart("compressed")) {
 282				switchOverToZLib(nextTag);
 283			} else if (nextTag.isStart("success")) {
 284				Log.d(Config.LOGTAG, account.getJid() + ": logged in");
 285				tagReader.readTag();
 286				tagReader.reset();
 287				sendStartStream();
 288				processStream(tagReader.readTag());
 289				break;
 290			} else if (nextTag.isStart("failure")) {
 291				tagReader.readElement(nextTag);
 292				changeStatus(Account.STATUS_UNAUTHORIZED);
 293			} else if (nextTag.isStart("challenge")) {
 294				String challange = tagReader.readElement(nextTag).getContent();
 295				Element response = new Element("response");
 296				response.setAttribute("xmlns",
 297						"urn:ietf:params:xml:ns:xmpp-sasl");
 298				response.setContent(CryptoHelper.saslDigestMd5(account,
 299						challange, mXmppConnectionService.getRNG()));
 300				tagWriter.writeElement(response);
 301			} else if (nextTag.isStart("enabled")) {
 302				Element enabled = tagReader.readElement(nextTag);
 303				if ("true".equals(enabled.getAttribute("resume"))) {
 304					this.streamId = enabled.getAttribute("id");
 305					Log.d(Config.LOGTAG, account.getJid()
 306							+ ": stream managment(" + smVersion
 307							+ ") enabled (resumable)");
 308				} else {
 309					Log.d(Config.LOGTAG, account.getJid()
 310							+ ": stream managment(" + smVersion + ") enabled");
 311				}
 312				this.lastSessionStarted = SystemClock.elapsedRealtime();
 313				this.stanzasReceived = 0;
 314				RequestPacket r = new RequestPacket(smVersion);
 315				tagWriter.writeStanzaAsync(r);
 316			} else if (nextTag.isStart("resumed")) {
 317				lastPaketReceived = SystemClock.elapsedRealtime();
 318				Element resumed = tagReader.readElement(nextTag);
 319				String h = resumed.getAttribute("h");
 320				try {
 321					int serverCount = Integer.parseInt(h);
 322					if (serverCount != stanzasSent) {
 323						Log.d(Config.LOGTAG, account.getJid()
 324								+ ": session resumed with lost packages");
 325						stanzasSent = serverCount;
 326					} else {
 327						Log.d(Config.LOGTAG, account.getJid()
 328								+ ": session resumed");
 329					}
 330					if (acknowledgedListener != null) {
 331						for (int i = 0; i < messageReceipts.size(); ++i) {
 332							if (serverCount >= messageReceipts.keyAt(i)) {
 333								acknowledgedListener.onMessageAcknowledged(
 334										account, messageReceipts.valueAt(i));
 335							}
 336						}
 337					}
 338					messageReceipts.clear();
 339				} catch (NumberFormatException e) {
 340
 341				}
 342				sendInitialPing();
 343
 344			} else if (nextTag.isStart("r")) {
 345				tagReader.readElement(nextTag);
 346				AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
 347				tagWriter.writeStanzaAsync(ack);
 348			} else if (nextTag.isStart("a")) {
 349				Element ack = tagReader.readElement(nextTag);
 350				lastPaketReceived = SystemClock.elapsedRealtime();
 351				int serverSequence = Integer.parseInt(ack.getAttribute("h"));
 352				String msgId = this.messageReceipts.get(serverSequence);
 353				if (msgId != null) {
 354					if (this.acknowledgedListener != null) {
 355						this.acknowledgedListener.onMessageAcknowledged(
 356								account, msgId);
 357					}
 358					this.messageReceipts.remove(serverSequence);
 359				}
 360			} else if (nextTag.isStart("failed")) {
 361				tagReader.readElement(nextTag);
 362				Log.d(Config.LOGTAG, account.getJid() + ": resumption failed");
 363				streamId = null;
 364				if (account.getStatus() != Account.STATUS_ONLINE) {
 365					sendBindRequest();
 366				}
 367			} else if (nextTag.isStart("iq")) {
 368				processIq(nextTag);
 369			} else if (nextTag.isStart("message")) {
 370				processMessage(nextTag);
 371			} else if (nextTag.isStart("presence")) {
 372				processPresence(nextTag);
 373			}
 374			nextTag = tagReader.readTag();
 375		}
 376		if (account.getStatus() == Account.STATUS_ONLINE) {
 377			account.setStatus(Account.STATUS_OFFLINE);
 378			if (statusListener != null) {
 379				statusListener.onStatusChanged(account);
 380			}
 381		}
 382	}
 383
 384	private void sendInitialPing() {
 385		Log.d(Config.LOGTAG, account.getJid() + ": sending intial ping");
 386		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 387		iq.setFrom(account.getFullJid());
 388		iq.addChild("ping", "urn:xmpp:ping");
 389		this.sendIqPacket(iq, new OnIqPacketReceived() {
 390
 391			@Override
 392			public void onIqPacketReceived(Account account, IqPacket packet) {
 393				Log.d(Config.LOGTAG, account.getJid()
 394						+ ": online with resource " + account.getResource());
 395				changeStatus(Account.STATUS_ONLINE);
 396			}
 397		});
 398	}
 399
 400	private Element processPacket(Tag currentTag, int packetType)
 401			throws XmlPullParserException, IOException {
 402		Element element;
 403		switch (packetType) {
 404			case PACKET_IQ:
 405				element = new IqPacket();
 406				break;
 407			case PACKET_MESSAGE:
 408				element = new MessagePacket();
 409				break;
 410			case PACKET_PRESENCE:
 411				element = new PresencePacket();
 412				break;
 413			default:
 414				return null;
 415		}
 416		element.setAttributes(currentTag.getAttributes());
 417		Tag nextTag = tagReader.readTag();
 418		if (nextTag == null) {
 419			throw new IOException("interrupted mid tag");
 420		}
 421		while (!nextTag.isEnd(element.getName())) {
 422			if (!nextTag.isNo()) {
 423				Element child = tagReader.readElement(nextTag);
 424				String type = currentTag.getAttribute("type");
 425				if (packetType == PACKET_IQ
 426						&& "jingle".equals(child.getName())
 427						&& ("set".equalsIgnoreCase(type) || "get"
 428						.equalsIgnoreCase(type))) {
 429					element = new JinglePacket();
 430					element.setAttributes(currentTag.getAttributes());
 431				}
 432				element.addChild(child);
 433			}
 434			nextTag = tagReader.readTag();
 435			if (nextTag == null) {
 436				throw new IOException("interrupted mid tag");
 437			}
 438		}
 439		++stanzasReceived;
 440		lastPaketReceived = SystemClock.elapsedRealtime();
 441		return element;
 442	}
 443
 444	private void processIq(Tag currentTag) throws XmlPullParserException,
 445			IOException {
 446		IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
 447
 448		if (packet.getId() == null) {
 449			return; // an iq packet without id is definitely invalid
 450		}
 451
 452		if (packet instanceof JinglePacket) {
 453			if (this.jingleListener != null) {
 454				this.jingleListener.onJinglePacketReceived(account,
 455						(JinglePacket) packet);
 456			}
 457		} else {
 458			if (packetCallbacks.containsKey(packet.getId())) {
 459				if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
 460					((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
 461							.onIqPacketReceived(account, packet);
 462				}
 463
 464				packetCallbacks.remove(packet.getId());
 465			} else if ((packet.getType() == IqPacket.TYPE_GET || packet
 466					.getType() == IqPacket.TYPE_SET)
 467					&& this.unregisteredIqListener != null) {
 468				this.unregisteredIqListener.onIqPacketReceived(account, packet);
 469			}
 470		}
 471	}
 472
 473	private void processMessage(Tag currentTag) throws XmlPullParserException,
 474			IOException {
 475		MessagePacket packet = (MessagePacket) processPacket(currentTag,
 476				PACKET_MESSAGE);
 477		String id = packet.getAttribute("id");
 478		if ((id != null) && (packetCallbacks.containsKey(id))) {
 479			if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
 480				((OnMessagePacketReceived) packetCallbacks.get(id))
 481						.onMessagePacketReceived(account, packet);
 482			}
 483			packetCallbacks.remove(id);
 484		} else if (this.messageListener != null) {
 485			this.messageListener.onMessagePacketReceived(account, packet);
 486		}
 487	}
 488
 489	private void processPresence(Tag currentTag) throws XmlPullParserException,
 490			IOException {
 491		PresencePacket packet = (PresencePacket) processPacket(currentTag,
 492				PACKET_PRESENCE);
 493		String id = packet.getAttribute("id");
 494		if ((id != null) && (packetCallbacks.containsKey(id))) {
 495			if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
 496				((OnPresencePacketReceived) packetCallbacks.get(id))
 497						.onPresencePacketReceived(account, packet);
 498			}
 499			packetCallbacks.remove(id);
 500		} else if (this.presenceListener != null) {
 501			this.presenceListener.onPresencePacketReceived(account, packet);
 502		}
 503	}
 504
 505	private void sendCompressionZlib() throws IOException {
 506		Element compress = new Element("compress");
 507		compress.setAttribute("xmlns", "http://jabber.org/protocol/compress");
 508		compress.addChild("method").setContent("zlib");
 509		tagWriter.writeElement(compress);
 510	}
 511
 512	private void switchOverToZLib(Tag currentTag)
 513			throws XmlPullParserException, IOException,
 514			NoSuchAlgorithmException {
 515		tagReader.readTag(); // read tag close
 516		tagWriter.setOutputStream(new ZLibOutputStream(tagWriter
 517				.getOutputStream()));
 518		tagReader
 519				.setInputStream(new ZLibInputStream(tagReader.getInputStream()));
 520
 521		sendStartStream();
 522		Log.d(Config.LOGTAG, account.getJid() + ": compression enabled");
 523		usingCompression = true;
 524		processStream(tagReader.readTag());
 525	}
 526
 527	private void sendStartTLS() throws IOException {
 528		Tag startTLS = Tag.empty("starttls");
 529		startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
 530		tagWriter.writeTag(startTLS);
 531	}
 532
 533	private SharedPreferences getPreferences() {
 534		return PreferenceManager
 535				.getDefaultSharedPreferences(applicationContext);
 536	}
 537
 538	private boolean enableLegacySSL() {
 539		return getPreferences().getBoolean("enable_legacy_ssl", false);
 540	}
 541
 542	private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
 543			IOException {
 544		tagReader.readTag();
 545		try {
 546			SSLContext sc = SSLContext.getInstance("TLS");
 547			sc.init(null,
 548					new X509TrustManager[]{this.mXmppConnectionService.getMemorizingTrustManager()},
 549					mXmppConnectionService.getRNG());
 550			SSLSocketFactory factory = sc.getSocketFactory();
 551
 552			if (factory == null) {
 553				throw new IOException("SSLSocketFactory was null");
 554			}
 555
 556			HostnameVerifier verifier = this.mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier());
 557
 558			if (socket == null || socket.isClosed()) {
 559				throw new IOException("socket null or closed");
 560			}
 561			final InetAddress address = socket.getInetAddress();
 562			if (address == null) {
 563				throw new IOException("socket address was null");
 564			}
 565
 566			final SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,address.getHostAddress(), socket.getPort(),true);
 567
 568			// Support all protocols except legacy SSL.
 569			// The min SDK version prevents us having to worry about SSLv2. In
 570			// future, this may be
 571			// true of SSLv3 as well.
 572			final String[] supportProtocols;
 573			if (enableLegacySSL()) {
 574				supportProtocols = sslSocket.getSupportedProtocols();
 575			} else {
 576				final List<String> supportedProtocols = new LinkedList<String>(
 577						Arrays.asList(sslSocket.getSupportedProtocols()));
 578				supportedProtocols.remove("SSLv3");
 579				supportProtocols = new String[supportedProtocols.size()];
 580				supportedProtocols.toArray(supportProtocols);
 581			}
 582			sslSocket.setEnabledProtocols(supportProtocols);
 583
 584			if (verifier != null
 585					&& !verifier.verify(account.getServer(),
 586					sslSocket.getSession())) {
 587				sslSocket.close();
 588				throw new IOException("host mismatch in TLS connection");
 589			}
 590			tagReader.setInputStream(sslSocket.getInputStream());
 591			tagWriter.setOutputStream(sslSocket.getOutputStream());
 592			sendStartStream();
 593			Log.d(Config.LOGTAG, account.getJid()
 594					+ ": TLS connection established");
 595			usingEncryption = true;
 596			processStream(tagReader.readTag());
 597			sslSocket.close();
 598		} catch (NoSuchAlgorithmException e1) {
 599			e1.printStackTrace();
 600		} catch (KeyManagementException e) {
 601			e.printStackTrace();
 602		}
 603	}
 604
 605	private void sendSaslAuthPlain() throws IOException {
 606		String saslString = CryptoHelper.saslPlain(account.getUsername(),
 607				account.getPassword());
 608		Element auth = new Element("auth");
 609		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
 610		auth.setAttribute("mechanism", "PLAIN");
 611		auth.setContent(saslString);
 612		tagWriter.writeElement(auth);
 613	}
 614
 615	private void sendSaslAuthDigestMd5() throws IOException {
 616		Element auth = new Element("auth");
 617		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
 618		auth.setAttribute("mechanism", "DIGEST-MD5");
 619		tagWriter.writeElement(auth);
 620	}
 621
 622	private void processStreamFeatures(Tag currentTag)
 623			throws XmlPullParserException, IOException {
 624		this.streamFeatures = tagReader.readElement(currentTag);
 625		if (this.streamFeatures.hasChild("starttls") && !usingEncryption) {
 626			sendStartTLS();
 627		} else if (compressionAvailable()) {
 628			sendCompressionZlib();
 629		} else if (this.streamFeatures.hasChild("register")
 630				&& account.isOptionSet(Account.OPTION_REGISTER)
 631				&& usingEncryption) {
 632			sendRegistryRequest();
 633		} else if (!this.streamFeatures.hasChild("register")
 634				&& account.isOptionSet(Account.OPTION_REGISTER)) {
 635			changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
 636			disconnect(true);
 637		} else if (this.streamFeatures.hasChild("mechanisms")
 638				&& shouldAuthenticate && usingEncryption) {
 639			List<String> mechanisms = extractMechanisms(streamFeatures
 640					.findChild("mechanisms"));
 641			if (mechanisms.contains("PLAIN")) {
 642				sendSaslAuthPlain();
 643			} else if (mechanisms.contains("DIGEST-MD5")) {
 644				sendSaslAuthDigestMd5();
 645			}
 646		} else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
 647				+ smVersion)
 648				&& streamId != null) {
 649			ResumePacket resume = new ResumePacket(this.streamId,
 650					stanzasReceived, smVersion);
 651			this.tagWriter.writeStanzaAsync(resume);
 652		} else if (this.streamFeatures.hasChild("bind") && shouldBind) {
 653			sendBindRequest();
 654		} else {
 655			Log.d(Config.LOGTAG, account.getJid()
 656					+ ": incompatible server. disconnecting");
 657			disconnect(true);
 658		}
 659	}
 660
 661	private boolean compressionAvailable() {
 662		if (!this.streamFeatures.hasChild("compression",
 663				"http://jabber.org/features/compress"))
 664			return false;
 665		if (!ZLibOutputStream.SUPPORTED)
 666			return false;
 667		if (!account.isOptionSet(Account.OPTION_USECOMPRESSION))
 668			return false;
 669
 670		Element compression = this.streamFeatures.findChild("compression",
 671				"http://jabber.org/features/compress");
 672		for (Element child : compression.getChildren()) {
 673			if (!"method".equals(child.getName()))
 674				continue;
 675
 676			if ("zlib".equalsIgnoreCase(child.getContent())) {
 677				return true;
 678			}
 679		}
 680		return false;
 681	}
 682
 683	private List<String> extractMechanisms(Element stream) {
 684		ArrayList<String> mechanisms = new ArrayList<String>(stream
 685				.getChildren().size());
 686		for (Element child : stream.getChildren()) {
 687			mechanisms.add(child.getContent());
 688		}
 689		return mechanisms;
 690	}
 691
 692	private void sendRegistryRequest() {
 693		IqPacket register = new IqPacket(IqPacket.TYPE_GET);
 694		register.query("jabber:iq:register");
 695		register.setTo(account.getServer());
 696		sendIqPacket(register, new OnIqPacketReceived() {
 697
 698			@Override
 699			public void onIqPacketReceived(Account account, IqPacket packet) {
 700				Element instructions = packet.query().findChild("instructions");
 701				if (packet.query().hasChild("username")
 702						&& (packet.query().hasChild("password"))) {
 703					IqPacket register = new IqPacket(IqPacket.TYPE_SET);
 704					Element username = new Element("username")
 705							.setContent(account.getUsername());
 706					Element password = new Element("password")
 707							.setContent(account.getPassword());
 708					register.query("jabber:iq:register").addChild(username);
 709					register.query().addChild(password);
 710					sendIqPacket(register, new OnIqPacketReceived() {
 711
 712						@Override
 713						public void onIqPacketReceived(Account account,
 714													   IqPacket packet) {
 715							if (packet.getType() == IqPacket.TYPE_RESULT) {
 716								account.setOption(Account.OPTION_REGISTER,
 717										false);
 718								changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
 719							} else if (packet.hasChild("error")
 720									&& (packet.findChild("error")
 721									.hasChild("conflict"))) {
 722								changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
 723							} else {
 724								changeStatus(Account.STATUS_REGISTRATION_FAILED);
 725								Log.d(Config.LOGTAG, packet.toString());
 726							}
 727							disconnect(true);
 728						}
 729					});
 730				} else {
 731					changeStatus(Account.STATUS_REGISTRATION_FAILED);
 732					disconnect(true);
 733					Log.d(Config.LOGTAG, account.getJid()
 734							+ ": could not register. instructions are"
 735							+ instructions.getContent());
 736				}
 737			}
 738		});
 739	}
 740
 741	private void sendBindRequest() throws IOException {
 742		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
 743		iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
 744				.addChild("resource").setContent(account.getResource());
 745		this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
 746			@Override
 747			public void onIqPacketReceived(Account account, IqPacket packet) {
 748				Element bind = packet.findChild("bind");
 749				if (bind != null) {
 750					Element jid = bind.findChild("jid");
 751					if (jid != null && jid.getContent() != null) {
 752						account.setResource(jid.getContent().split("/", 2)[1]);
 753						if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
 754							smVersion = 3;
 755							EnablePacket enable = new EnablePacket(smVersion);
 756							tagWriter.writeStanzaAsync(enable);
 757							stanzasSent = 0;
 758							messageReceipts.clear();
 759						} else if (streamFeatures.hasChild("sm",
 760								"urn:xmpp:sm:2")) {
 761							smVersion = 2;
 762							EnablePacket enable = new EnablePacket(smVersion);
 763							tagWriter.writeStanzaAsync(enable);
 764							stanzasSent = 0;
 765							messageReceipts.clear();
 766						}
 767						sendServiceDiscoveryInfo(account.getServer());
 768						sendServiceDiscoveryItems(account.getServer());
 769						if (bindListener != null) {
 770							bindListener.onBind(account);
 771						}
 772						sendInitialPing();
 773					} else {
 774						disconnect(true);
 775					}
 776				} else {
 777					disconnect(true);
 778				}
 779			}
 780		});
 781		if (this.streamFeatures.hasChild("session")) {
 782			Log.d(Config.LOGTAG, account.getJid()
 783					+ ": sending deprecated session");
 784			IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
 785			startSession.addChild("session",
 786					"urn:ietf:params:xml:ns:xmpp-session");
 787			this.sendUnboundIqPacket(startSession, null);
 788		}
 789	}
 790
 791	private void sendServiceDiscoveryInfo(final String server) {
 792		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 793		iq.setTo(server);
 794		iq.query("http://jabber.org/protocol/disco#info");
 795		this.sendIqPacket(iq, new OnIqPacketReceived() {
 796
 797			@Override
 798			public void onIqPacketReceived(Account account, IqPacket packet) {
 799				List<Element> elements = packet.query().getChildren();
 800				List<String> features = new ArrayList<String>();
 801				for (int i = 0; i < elements.size(); ++i) {
 802					if (elements.get(i).getName().equals("feature")) {
 803						features.add(elements.get(i).getAttribute("var"));
 804					}
 805				}
 806				disco.put(server, features);
 807
 808				if (account.getServer().equals(server)) {
 809					enableAdvancedStreamFeatures();
 810				}
 811			}
 812		});
 813	}
 814
 815	private void enableAdvancedStreamFeatures() {
 816		if (getFeatures().carbons()) {
 817			sendEnableCarbons();
 818		}
 819	}
 820
 821	private void sendServiceDiscoveryItems(final String server) {
 822		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 823		iq.setTo(server);
 824		iq.query("http://jabber.org/protocol/disco#items");
 825		this.sendIqPacket(iq, new OnIqPacketReceived() {
 826
 827			@Override
 828			public void onIqPacketReceived(Account account, IqPacket packet) {
 829				List<Element> elements = packet.query().getChildren();
 830				for (int i = 0; i < elements.size(); ++i) {
 831					if (elements.get(i).getName().equals("item")) {
 832						String jid = elements.get(i).getAttribute("jid");
 833						sendServiceDiscoveryInfo(jid);
 834					}
 835				}
 836			}
 837		});
 838	}
 839
 840	private void sendEnableCarbons() {
 841		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
 842		iq.addChild("enable", "urn:xmpp:carbons:2");
 843		this.sendIqPacket(iq, new OnIqPacketReceived() {
 844
 845			@Override
 846			public void onIqPacketReceived(Account account, IqPacket packet) {
 847				if (!packet.hasChild("error")) {
 848					Log.d(Config.LOGTAG, account.getJid()
 849							+ ": successfully enabled carbons");
 850				} else {
 851					Log.d(Config.LOGTAG, account.getJid()
 852							+ ": error enableing carbons " + packet.toString());
 853				}
 854			}
 855		});
 856	}
 857
 858	private void processStreamError(Tag currentTag)
 859			throws XmlPullParserException, IOException {
 860		Element streamError = tagReader.readElement(currentTag);
 861		if (streamError != null && streamError.hasChild("conflict")) {
 862			String resource = account.getResource().split("\\.")[0];
 863			account.setResource(resource + "." + nextRandomId());
 864			Log.d(Config.LOGTAG,
 865					account.getJid() + ": switching resource due to conflict ("
 866							+ account.getResource() + ")");
 867		}
 868	}
 869
 870	private void sendStartStream() throws IOException {
 871		Tag stream = Tag.start("stream:stream");
 872		stream.setAttribute("from", account.getJid());
 873		stream.setAttribute("to", account.getServer());
 874		stream.setAttribute("version", "1.0");
 875		stream.setAttribute("xml:lang", "en");
 876		stream.setAttribute("xmlns", "jabber:client");
 877		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
 878		tagWriter.writeTag(stream);
 879	}
 880
 881	private String nextRandomId() {
 882		return new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
 883	}
 884
 885	public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
 886		if (packet.getId() == null) {
 887			String id = nextRandomId();
 888			packet.setAttribute("id", id);
 889		}
 890		packet.setFrom(account.getFullJid());
 891		this.sendPacket(packet, callback);
 892	}
 893
 894	public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
 895		if (packet.getId() == null) {
 896			String id = nextRandomId();
 897			packet.setAttribute("id", id);
 898		}
 899		this.sendPacket(packet, callback);
 900	}
 901
 902	public void sendMessagePacket(MessagePacket packet) {
 903		this.sendPacket(packet, null);
 904	}
 905
 906	public void sendPresencePacket(PresencePacket packet) {
 907		this.sendPacket(packet, null);
 908	}
 909
 910	private synchronized void sendPacket(final AbstractStanza packet,
 911										 PacketReceived callback) {
 912		if (packet.getName().equals("iq") || packet.getName().equals("message")
 913				|| packet.getName().equals("presence")) {
 914			++stanzasSent;
 915		}
 916		tagWriter.writeStanzaAsync(packet);
 917		if (packet instanceof MessagePacket && packet.getId() != null
 918				&& this.streamId != null) {
 919			Log.d(Config.LOGTAG, "request delivery report for stanza "
 920					+ stanzasSent);
 921			this.messageReceipts.put(stanzasSent, packet.getId());
 922			tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
 923		}
 924		if (callback != null) {
 925			if (packet.getId() == null) {
 926				packet.setId(nextRandomId());
 927			}
 928			packetCallbacks.put(packet.getId(), callback);
 929		}
 930	}
 931
 932	public void sendPing() {
 933		if (streamFeatures.hasChild("sm")) {
 934			tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
 935		} else {
 936			IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
 937			iq.setFrom(account.getFullJid());
 938			iq.addChild("ping", "urn:xmpp:ping");
 939			this.sendIqPacket(iq, null);
 940		}
 941		this.lastPingSent = SystemClock.elapsedRealtime();
 942	}
 943
 944	public void setOnMessagePacketReceivedListener(
 945			OnMessagePacketReceived listener) {
 946		this.messageListener = listener;
 947	}
 948
 949	public void setOnUnregisteredIqPacketReceivedListener(
 950			OnIqPacketReceived listener) {
 951		this.unregisteredIqListener = listener;
 952	}
 953
 954	public void setOnPresencePacketReceivedListener(
 955			OnPresencePacketReceived listener) {
 956		this.presenceListener = listener;
 957	}
 958
 959	public void setOnJinglePacketReceivedListener(
 960			OnJinglePacketReceived listener) {
 961		this.jingleListener = listener;
 962	}
 963
 964	public void setOnStatusChangedListener(OnStatusChanged listener) {
 965		this.statusListener = listener;
 966	}
 967
 968	public void setOnBindListener(OnBindListener listener) {
 969		this.bindListener = listener;
 970	}
 971
 972	public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) {
 973		this.acknowledgedListener = listener;
 974	}
 975
 976	public void disconnect(boolean force) {
 977		Log.d(Config.LOGTAG, account.getJid() + ": disconnecting");
 978		try {
 979			if (force) {
 980				socket.close();
 981				return;
 982			}
 983			new Thread(new Runnable() {
 984
 985				@Override
 986				public void run() {
 987					if (tagWriter.isActive()) {
 988						tagWriter.finish();
 989						try {
 990							while (!tagWriter.finished()) {
 991								Log.d(Config.LOGTAG, "not yet finished");
 992								Thread.sleep(100);
 993							}
 994							tagWriter.writeTag(Tag.end("stream:stream"));
 995							socket.close();
 996						} catch (IOException e) {
 997							Log.d(Config.LOGTAG,
 998									"io exception during disconnect");
 999						} catch (InterruptedException e) {
1000							Log.d(Config.LOGTAG, "interrupted");
1001						}
1002					}
1003				}
1004			}).start();
1005		} catch (IOException e) {
1006			Log.d(Config.LOGTAG, "io exception during disconnect");
1007		}
1008	}
1009
1010	public List<String> findDiscoItemsByFeature(String feature) {
1011		List<String> items = new ArrayList<String>();
1012		for (Entry<String, List<String>> cursor : disco.entrySet()) {
1013			if (cursor.getValue().contains(feature)) {
1014				items.add(cursor.getKey());
1015			}
1016		}
1017		return items;
1018	}
1019
1020	public String findDiscoItemByFeature(String feature) {
1021		List<String> items = findDiscoItemsByFeature(feature);
1022		if (items.size() >= 1) {
1023			return items.get(0);
1024		}
1025		return null;
1026	}
1027
1028	public void r() {
1029		this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
1030	}
1031
1032	public String getMucServer() {
1033		return findDiscoItemByFeature("http://jabber.org/protocol/muc");
1034	}
1035
1036	public int getTimeToNextAttempt() {
1037		int interval = (int) (25 * Math.pow(1.5, attempt));
1038		int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
1039		return interval - secondsSinceLast;
1040	}
1041
1042	public int getAttempt() {
1043		return this.attempt;
1044	}
1045
1046	public Features getFeatures() {
1047		return this.features;
1048	}
1049
1050	public long getLastSessionEstablished() {
1051		long diff;
1052		if (this.lastSessionStarted == 0) {
1053			diff = SystemClock.elapsedRealtime() - this.lastConnect;
1054		} else {
1055			diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
1056		}
1057		return System.currentTimeMillis() - diff;
1058	}
1059
1060	public long getLastConnect() {
1061		return this.lastConnect;
1062	}
1063
1064	public long getLastPingSent() {
1065		return this.lastPingSent;
1066	}
1067
1068	public long getLastPacketReceived() {
1069		return this.lastPaketReceived;
1070	}
1071
1072	public void sendActive() {
1073		this.sendPacket(new ActivePacket(), null);
1074	}
1075
1076	public void sendInactive() {
1077		this.sendPacket(new InactivePacket(), null);
1078	}
1079
1080	public class Features {
1081		XmppConnection connection;
1082
1083		public Features(XmppConnection connection) {
1084			this.connection = connection;
1085		}
1086
1087		private boolean hasDiscoFeature(String server, String feature) {
1088			if (!connection.disco.containsKey(server)) {
1089				return false;
1090			}
1091			return connection.disco.get(server).contains(feature);
1092		}
1093
1094		public boolean carbons() {
1095			return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
1096		}
1097
1098		public boolean sm() {
1099			return streamId != null;
1100		}
1101
1102		public boolean csi() {
1103			if (connection.streamFeatures == null) {
1104				return false;
1105			} else {
1106				return connection.streamFeatures.hasChild("csi",
1107						"urn:xmpp:csi:0");
1108			}
1109		}
1110
1111		public boolean pubsub() {
1112			return hasDiscoFeature(account.getServer(),
1113					"http://jabber.org/protocol/pubsub#publish");
1114		}
1115
1116		public boolean mam() {
1117			return hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0");
1118		}
1119
1120		public boolean rosterVersioning() {
1121			if (connection.streamFeatures == null) {
1122				return false;
1123			} else {
1124				return connection.streamFeatures.hasChild("ver");
1125			}
1126		}
1127
1128		public boolean streamhost() {
1129			return connection
1130					.findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null;
1131		}
1132
1133		public boolean compression() {
1134			return connection.usingCompression;
1135		}
1136	}
1137}