XmppConnection.java

  1package eu.siacs.conversations.xmpp;
  2
  3import java.io.IOException;
  4import java.io.InputStream;
  5import java.io.OutputStream;
  6import java.math.BigInteger;
  7import java.net.Socket;
  8import java.net.UnknownHostException;
  9import java.security.KeyManagementException;
 10import java.security.KeyStore;
 11import java.security.KeyStoreException;
 12import java.security.MessageDigest;
 13import java.security.NoSuchAlgorithmException;
 14import java.security.SecureRandom;
 15import java.security.cert.CertPathValidatorException;
 16import java.security.cert.CertificateException;
 17import java.security.cert.X509Certificate;
 18import java.util.ArrayList;
 19import java.util.HashSet;
 20import java.util.Hashtable;
 21import java.util.List;
 22
 23import javax.net.ssl.SSLContext;
 24import javax.net.ssl.SSLSocket;
 25import javax.net.ssl.SSLSocketFactory;
 26import javax.net.ssl.TrustManager;
 27import javax.net.ssl.TrustManagerFactory;
 28import javax.net.ssl.X509TrustManager;
 29
 30import org.json.JSONException;
 31import org.xmlpull.v1.XmlPullParserException;
 32
 33import android.os.Bundle;
 34import android.os.PowerManager;
 35import android.os.PowerManager.WakeLock;
 36import android.os.SystemClock;
 37import android.util.Log;
 38import eu.siacs.conversations.entities.Account;
 39import eu.siacs.conversations.utils.CryptoHelper;
 40import eu.siacs.conversations.utils.DNSHelper;
 41import eu.siacs.conversations.xml.Element;
 42import eu.siacs.conversations.xml.Tag;
 43import eu.siacs.conversations.xml.TagWriter;
 44import eu.siacs.conversations.xml.XmlReader;
 45import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
 46import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 47import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 48import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 49import eu.siacs.conversations.xmpp.stanzas.streammgmt.AckPacket;
 50import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
 51import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
 52import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
 53
 54public class XmppConnection implements Runnable {
 55
 56	protected Account account;
 57	private static final String LOGTAG = "xmppService";
 58
 59	private PowerManager.WakeLock wakeLock;
 60
 61	private SecureRandom random = new SecureRandom();
 62
 63	private Socket socket;
 64	private XmlReader tagReader;
 65	private TagWriter tagWriter;
 66
 67	private boolean shouldBind = true;
 68	private boolean shouldAuthenticate = true;
 69	private Element streamFeatures;
 70	private HashSet<String> discoFeatures = new HashSet<String>();
 71	private List<String> discoItems = new ArrayList<String>();
 72	
 73	private String streamId = null;
 74	
 75	private int stanzasReceived = 0;
 76	private int stanzasSent = 0;
 77	
 78	public long lastPaketReceived = 0;
 79	public long lastPingSent = 0;
 80	public long lastConnect = 0;
 81	public long lastSessionStarted = 0;
 82
 83	private static final int PACKET_IQ = 0;
 84	private static final int PACKET_MESSAGE = 1;
 85	private static final int PACKET_PRESENCE = 2;
 86
 87	private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
 88	private OnPresencePacketReceived presenceListener = null;
 89	private OnIqPacketReceived unregisteredIqListener = null;
 90	private OnMessagePacketReceived messageListener = null;
 91	private OnStatusChanged statusListener = null;
 92	private OnTLSExceptionReceived tlsListener = null;
 93	private OnBindListener bindListener = null;
 94
 95	public XmppConnection(Account account, WakeLock wakeLock) {
 96		this.account = account;
 97		this.wakeLock = wakeLock;
 98		tagReader = new XmlReader(wakeLock);
 99		tagWriter = new TagWriter();
100	}
101
102	protected void changeStatus(int nextStatus) {
103		if (account.getStatus() != nextStatus) {
104			if ((nextStatus == Account.STATUS_OFFLINE)&&(account.getStatus() != Account.STATUS_CONNECTING)&&(account.getStatus() != Account.STATUS_ONLINE)) {
105				return;
106			}
107			account.setStatus(nextStatus);
108			if (statusListener != null) {
109				statusListener.onStatusChanged(account);
110			}
111		}
112	}
113
114	protected void connect() {
115		Log.d(LOGTAG,account.getJid()+ ": connecting");
116		lastConnect = SystemClock.elapsedRealtime();
117		try {
118			shouldAuthenticate = shouldBind = !account.isOptionSet(Account.OPTION_REGISTER);
119			tagReader = new XmlReader(wakeLock);
120			tagWriter = new TagWriter();
121			packetCallbacks.clear();
122			this.changeStatus(Account.STATUS_CONNECTING);
123			Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
124			String srvRecordServer = namePort.getString("name");
125			int srvRecordPort = namePort.getInt("port");
126			if (srvRecordServer != null) {
127				Log.d(LOGTAG, account.getJid() + ": using values from dns "
128						+ srvRecordServer + ":" + srvRecordPort);
129				socket = new Socket(srvRecordServer, srvRecordPort);
130			} else {
131				socket = new Socket(account.getServer(), 5222);
132			}
133			OutputStream out = socket.getOutputStream();
134			tagWriter.setOutputStream(out);
135			InputStream in = socket.getInputStream();
136			tagReader.setInputStream(in);
137			tagWriter.beginDocument();
138			sendStartStream();
139			Tag nextTag;
140			while ((nextTag = tagReader.readTag()) != null) {
141				if (nextTag.isStart("stream")) {
142					processStream(nextTag);
143					break;
144				} else {
145					Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName());
146					return;
147				}
148			}
149			if (socket.isConnected()) {
150				socket.close();
151			}
152		} catch (UnknownHostException e) {
153			this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
154			if (wakeLock.isHeld()) {
155				wakeLock.release();
156			}
157			return;
158		} catch (IOException e) {
159			if (account.getStatus() != Account.STATUS_TLS_ERROR) {
160				this.changeStatus(Account.STATUS_OFFLINE);
161			}
162			if (wakeLock.isHeld()) {
163				wakeLock.release();
164			}
165			return;
166		} catch (XmlPullParserException e) {
167			this.changeStatus(Account.STATUS_OFFLINE);
168			Log.d(LOGTAG, "xml exception " + e.getMessage());
169			if (wakeLock.isHeld()) {
170				wakeLock.release();
171			}
172			return;
173		}
174
175	}
176
177	@Override
178	public void run() {
179		connect();
180	}
181
182	private void processStream(Tag currentTag) throws XmlPullParserException,
183			IOException {
184		Tag nextTag = tagReader.readTag();
185		while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
186			if (nextTag.isStart("error")) {
187				processStreamError(nextTag);
188			} else if (nextTag.isStart("features")) {
189				processStreamFeatures(nextTag);
190				if ((streamFeatures.getChildren().size() == 1)
191						&& (streamFeatures.hasChild("starttls"))
192						&& (!account.isOptionSet(Account.OPTION_USETLS))) {
193					changeStatus(Account.STATUS_SERVER_REQUIRES_TLS);
194				}
195			} else if (nextTag.isStart("proceed")) {
196				switchOverToTls(nextTag);
197			} else if (nextTag.isStart("success")) {
198				Log.d(LOGTAG, account.getJid()
199						+ ": logged in");
200				tagReader.readTag();
201				tagReader.reset();
202				sendStartStream();
203				processStream(tagReader.readTag());
204				break;
205			} else if (nextTag.isStart("failure")) {
206				tagReader.readElement(nextTag);
207				changeStatus(Account.STATUS_UNAUTHORIZED);
208			} else if (nextTag.isStart("enabled")) {
209				this.stanzasSent = 0;
210				Element enabled = tagReader.readElement(nextTag);
211				if ("true".equals(enabled.getAttribute("resume"))) {
212					this.streamId = enabled.getAttribute("id");
213					Log.d(LOGTAG,account.getJid()+": stream managment enabled (resumable)");
214				} else {
215					Log.d(LOGTAG,account.getJid()+": stream managment enabled");
216				}
217				this.lastSessionStarted = SystemClock.elapsedRealtime();
218				this.stanzasReceived = 0;
219				RequestPacket r = new RequestPacket();
220				tagWriter.writeStanzaAsync(r);
221			} else if (nextTag.isStart("resumed")) {
222				tagReader.readElement(nextTag);
223				sendPing();
224				changeStatus(Account.STATUS_ONLINE);
225				Log.d(LOGTAG,account.getJid()+": session resumed");
226			} else if (nextTag.isStart("r")) {
227				tagReader.readElement(nextTag);
228				AckPacket ack = new AckPacket(this.stanzasReceived);
229				//Log.d(LOGTAG,ack.toString());
230				tagWriter.writeStanzaAsync(ack);
231			} else if (nextTag.isStart("a")) {
232				Element ack = tagReader.readElement(nextTag);
233				lastPaketReceived = SystemClock.elapsedRealtime();
234				int serverSequence = Integer.parseInt(ack.getAttribute("h"));
235				if (serverSequence>this.stanzasSent) {
236					this.stanzasSent = serverSequence;
237				}
238				//Log.d(LOGTAG,"server ack"+ack.toString()+" ("+this.stanzasSent+")");
239			} else if (nextTag.isStart("failed")) {
240				tagReader.readElement(nextTag);
241				Log.d(LOGTAG,account.getJid()+": resumption failed");
242				streamId = null;
243				if (account.getStatus() != Account.STATUS_ONLINE) {
244					sendBindRequest();
245				}
246			} else if (nextTag.isStart("iq")) {
247				processIq(nextTag);
248			} else if (nextTag.isStart("message")) {
249				processMessage(nextTag);
250			} else if (nextTag.isStart("presence")) {
251				processPresence(nextTag);
252			} else {
253				Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName()
254						+ " as child of " + currentTag.getName());
255			}
256			nextTag = tagReader.readTag();
257		}
258		if (account.getStatus() == Account.STATUS_ONLINE) {
259			account.setStatus(Account.STATUS_OFFLINE);
260			if (statusListener != null) {
261				statusListener.onStatusChanged(account);
262			}
263		}
264	}
265
266	private Element processPacket(Tag currentTag, int packetType)
267			throws XmlPullParserException, IOException {
268		Element element;
269		switch (packetType) {
270		case PACKET_IQ:
271			element = new IqPacket();
272			break;
273		case PACKET_MESSAGE:
274			element = new MessagePacket();
275			break;
276		case PACKET_PRESENCE:
277			element = new PresencePacket();
278			break;
279		default:
280			return null;
281		}
282		element.setAttributes(currentTag.getAttributes());
283		Tag nextTag = tagReader.readTag();
284		while (!nextTag.isEnd(element.getName())) {
285			if (!nextTag.isNo()) {
286				Element child = tagReader.readElement(nextTag);
287				element.addChild(child);
288			}
289			nextTag = tagReader.readTag();
290		}
291		++stanzasReceived;
292		lastPaketReceived = SystemClock.elapsedRealtime();
293		return element;
294	}
295
296	private void processIq(Tag currentTag) throws XmlPullParserException,
297			IOException {
298		IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
299		if (packetCallbacks.containsKey(packet.getId())) {
300			if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
301				((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
302						.onIqPacketReceived(account, packet);
303			}
304
305			packetCallbacks.remove(packet.getId());
306		} else if (this.unregisteredIqListener != null) {
307			this.unregisteredIqListener.onIqPacketReceived(account, packet);
308		}
309	}
310
311	private void processMessage(Tag currentTag) throws XmlPullParserException,
312			IOException {
313		MessagePacket packet = (MessagePacket) processPacket(currentTag,
314				PACKET_MESSAGE);
315		String id = packet.getAttribute("id");
316		if ((id != null) && (packetCallbacks.containsKey(id))) {
317			if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
318				((OnMessagePacketReceived) packetCallbacks.get(id))
319						.onMessagePacketReceived(account, packet);
320			}
321			packetCallbacks.remove(id);
322		} else if (this.messageListener != null) {
323			this.messageListener.onMessagePacketReceived(account, packet);
324		}
325	}
326
327	private void processPresence(Tag currentTag) throws XmlPullParserException,
328			IOException {
329		PresencePacket packet = (PresencePacket) processPacket(currentTag,
330				PACKET_PRESENCE);
331		String id = packet.getAttribute("id");
332		if ((id != null) && (packetCallbacks.containsKey(id))) {
333			if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
334				((OnPresencePacketReceived) packetCallbacks.get(id))
335						.onPresencePacketReceived(account, packet);
336			}
337			packetCallbacks.remove(id);
338		} else if (this.presenceListener != null) {
339			this.presenceListener.onPresencePacketReceived(account, packet);
340		}
341	}
342
343	private void sendStartTLS() throws IOException {
344		Tag startTLS = Tag.empty("starttls");
345		startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
346		tagWriter.writeTag(startTLS);
347	}
348
349	private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
350			IOException {
351		Tag nextTag = tagReader.readTag(); // should be proceed end tag
352		try {
353			SSLContext sc = SSLContext.getInstance("TLS");
354			TrustManagerFactory tmf = TrustManagerFactory
355					.getInstance(TrustManagerFactory.getDefaultAlgorithm());
356			// Initialise the TMF as you normally would, for example:
357			// tmf.in
358			try {
359				tmf.init((KeyStore) null);
360			} catch (KeyStoreException e1) {
361				// TODO Auto-generated catch block
362				e1.printStackTrace();
363			}
364
365			TrustManager[] trustManagers = tmf.getTrustManagers();
366			final X509TrustManager origTrustmanager = (X509TrustManager) trustManagers[0];
367
368			TrustManager[] wrappedTrustManagers = new TrustManager[] { new X509TrustManager() {
369
370				@Override
371				public void checkClientTrusted(X509Certificate[] chain,
372						String authType) throws CertificateException {
373					origTrustmanager.checkClientTrusted(chain, authType);
374				}
375
376				@Override
377				public void checkServerTrusted(X509Certificate[] chain,
378						String authType) throws CertificateException {
379					try {
380						origTrustmanager.checkServerTrusted(chain, authType);
381					} catch (CertificateException e) {
382						if (e.getCause() instanceof CertPathValidatorException) {
383							String sha;
384							try {
385								MessageDigest sha1 = MessageDigest.getInstance("SHA1");
386								sha1.update(chain[0].getEncoded());
387								sha = CryptoHelper.bytesToHex(sha1.digest());
388								if (!sha.equals(account.getSSLFingerprint())) {
389									changeStatus(Account.STATUS_TLS_ERROR);
390									if (tlsListener!=null) {
391										tlsListener.onTLSExceptionReceived(sha,account);
392									}
393									throw new CertificateException();
394								}
395							} catch (NoSuchAlgorithmException e1) {
396								// TODO Auto-generated catch block
397								e1.printStackTrace();
398							}
399						} else {
400							throw new CertificateException();
401						}
402					}
403				}
404
405				@Override
406				public X509Certificate[] getAcceptedIssuers() {
407					return origTrustmanager.getAcceptedIssuers();
408				}
409
410			} };
411			sc.init(null, wrappedTrustManagers, null);
412			SSLSocketFactory factory = sc.getSocketFactory();
413			SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
414						socket.getInetAddress().getHostAddress(), socket.getPort(),
415						true);
416			tagReader.setInputStream(sslSocket.getInputStream());
417			tagWriter.setOutputStream(sslSocket.getOutputStream());
418			sendStartStream();
419			Log.d(LOGTAG,account.getJid()+": TLS connection established");
420			processStream(tagReader.readTag());
421			sslSocket.close();
422		} catch (NoSuchAlgorithmException e1) {
423			// TODO Auto-generated catch block
424			e1.printStackTrace();
425		} catch (KeyManagementException e) {
426			// TODO Auto-generated catch block
427			e.printStackTrace();
428		}
429	}
430
431	private void sendSaslAuth() throws IOException, XmlPullParserException {
432		String saslString = CryptoHelper.saslPlain(account.getUsername(),
433				account.getPassword());
434		Element auth = new Element("auth");
435		auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
436		auth.setAttribute("mechanism", "PLAIN");
437		auth.setContent(saslString);
438		tagWriter.writeElement(auth);
439	}
440
441	private void processStreamFeatures(Tag currentTag)
442			throws XmlPullParserException, IOException {
443		this.streamFeatures = tagReader.readElement(currentTag);
444		if (this.streamFeatures.hasChild("starttls")
445				&& account.isOptionSet(Account.OPTION_USETLS)) {
446			sendStartTLS();
447		} else if (this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) {
448				sendRegistryRequest();
449		} else if (!this.streamFeatures.hasChild("register")&&(account.isOptionSet(Account.OPTION_REGISTER))) {
450			changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
451			disconnect(true);
452		} else if (this.streamFeatures.hasChild("mechanisms")
453				&& shouldAuthenticate) {
454			sendSaslAuth();
455		} else if (this.streamFeatures.hasChild("sm") && streamId != null) {
456			Log.d(LOGTAG,"found old stream id. trying to remuse");
457			ResumePacket resume = new ResumePacket(this.streamId,stanzasReceived);
458			this.tagWriter.writeStanzaAsync(resume);
459		} else if (this.streamFeatures.hasChild("bind") && shouldBind) {
460			sendBindRequest();
461			if (this.streamFeatures.hasChild("session")) {
462				Log.d(LOGTAG,"sending session");
463				IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
464				startSession.addChild("session","urn:ietf:params:xml:ns:xmpp-session"); //setContent("")
465				this.sendIqPacket(startSession, null);
466			}
467		}
468	}
469
470	private void sendRegistryRequest() {
471		IqPacket register = new IqPacket(IqPacket.TYPE_GET);
472		register.query("jabber:iq:register");
473		register.setTo(account.getServer());
474		sendIqPacket(register, new OnIqPacketReceived() {
475			
476			@Override
477			public void onIqPacketReceived(Account account, IqPacket packet) {
478				Element instructions = packet.query().findChild("instructions");
479				if (packet.query().hasChild("username")&&(packet.query().hasChild("password"))) {
480					IqPacket register = new IqPacket(IqPacket.TYPE_SET);
481					Element username = new Element("username").setContent(account.getUsername());
482					Element password = new Element("password").setContent(account.getPassword());
483					register.query("jabber:iq:register").addChild(username);
484					register.query().addChild(password);
485					sendIqPacket(register, new OnIqPacketReceived() {
486						
487						@Override
488						public void onIqPacketReceived(Account account, IqPacket packet) {
489							if (packet.getType()==IqPacket.TYPE_RESULT) {
490								account.setOption(Account.OPTION_REGISTER, false);
491								changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
492							} else if (packet.hasChild("error")&&(packet.findChild("error").hasChild("conflict"))){
493								changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
494							} else {
495								changeStatus(Account.STATUS_REGISTRATION_FAILED);
496								Log.d(LOGTAG,packet.toString());
497							}
498							disconnect(true);
499						}
500					});
501				} else {
502					changeStatus(Account.STATUS_REGISTRATION_FAILED);
503					disconnect(true);
504					Log.d(LOGTAG,account.getJid()+": could not register. instructions are"+instructions.getContent());
505				}
506			}
507		});
508	}
509
510	private void sendInitialPresence() {
511		PresencePacket packet = new PresencePacket();
512		packet.setAttribute("from", account.getFullJid());
513		if (account.getKeys().has("pgp_signature")) {
514			try {
515				String signature = account.getKeys().getString("pgp_signature");
516				packet.addChild("status").setContent("online");
517				packet.addChild("x","jabber:x:signed").setContent(signature);
518			} catch (JSONException e) {
519				//
520			}
521		}
522		this.sendPresencePacket(packet);
523	}
524
525	private void sendBindRequest() throws IOException {
526		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
527		iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind").addChild("resource").setContent(account.getResource());
528		this.sendIqPacket(iq, new OnIqPacketReceived() {
529			@Override
530			public void onIqPacketReceived(Account account, IqPacket packet) {
531				String resource = packet.findChild("bind").findChild("jid")
532						.getContent().split("/")[1];
533				account.setResource(resource);
534				if (streamFeatures.hasChild("sm")) {
535					EnablePacket enable = new EnablePacket();
536					tagWriter.writeStanzaAsync(enable);
537				}
538				sendInitialPresence();
539				sendServiceDiscoveryInfo();
540				sendServiceDiscoveryItems();
541				if (bindListener !=null) {
542					bindListener.onBind(account);
543				}
544				changeStatus(Account.STATUS_ONLINE);
545			}
546		});
547	}
548
549	private void sendServiceDiscoveryInfo() {
550		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
551		iq.setTo(account.getServer());
552		iq.query("http://jabber.org/protocol/disco#info");
553		this.sendIqPacket(iq, new OnIqPacketReceived() {
554
555			@Override
556			public void onIqPacketReceived(Account account, IqPacket packet) {
557					List<Element> elements = packet.query().getChildren();
558					for (int i = 0; i < elements.size(); ++i) {
559						if (elements.get(i).getName().equals("feature")) {
560							discoFeatures.add(elements.get(i).getAttribute(
561									"var"));
562						}
563					}
564				if (discoFeatures.contains("urn:xmpp:carbons:2")) {
565					sendEnableCarbons();
566				}
567			}
568		});
569	}
570	private void sendServiceDiscoveryItems() {
571		IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
572		iq.setTo(account.getServer());
573		iq.query("http://jabber.org/protocol/disco#items");
574		this.sendIqPacket(iq, new OnIqPacketReceived() {
575
576			@Override
577			public void onIqPacketReceived(Account account, IqPacket packet) {
578					List<Element> elements = packet.query().getChildren();
579					for (int i = 0; i < elements.size(); ++i) {
580						if (elements.get(i).getName().equals("item")) {
581							discoItems.add(elements.get(i).getAttribute(
582									"jid"));
583						}
584					}
585			}
586		});
587	}
588
589	private void sendEnableCarbons() {
590		IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
591		iq.addChild("enable","urn:xmpp:carbons:2");
592		this.sendIqPacket(iq, new OnIqPacketReceived() {
593
594			@Override
595			public void onIqPacketReceived(Account account, IqPacket packet) {
596				if (!packet.hasChild("error")) {
597					Log.d(LOGTAG, account.getJid()
598							+ ": successfully enabled carbons");
599				} else {
600					Log.d(LOGTAG, account.getJid()
601							+ ": error enableing carbons " + packet.toString());
602				}
603			}
604		});
605	}
606
607	private void processStreamError(Tag currentTag) {
608		Log.d(LOGTAG, "processStreamError");
609	}
610
611	private void sendStartStream() throws IOException {
612		Tag stream = Tag.start("stream:stream");
613		stream.setAttribute("from", account.getJid());
614		stream.setAttribute("to", account.getServer());
615		stream.setAttribute("version", "1.0");
616		stream.setAttribute("xml:lang", "en");
617		stream.setAttribute("xmlns", "jabber:client");
618		stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
619		tagWriter.writeTag(stream);
620	}
621
622	private String nextRandomId() {
623		return new BigInteger(50, random).toString(32);
624	}
625
626	public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
627		String id = nextRandomId();
628		packet.setAttribute("id", id);
629		this.sendPacket(packet, callback);
630	}
631
632	public void sendMessagePacket(MessagePacket packet) {
633		this.sendPacket(packet, null);
634	}
635
636	public void sendMessagePacket(MessagePacket packet,
637			OnMessagePacketReceived callback) {
638		this.sendPacket(packet, callback);
639	}
640
641	public void sendPresencePacket(PresencePacket packet) {
642		this.sendPacket(packet, null);
643	}
644
645	public void sendPresencePacket(PresencePacket packet,
646			OnPresencePacketReceived callback) {
647		this.sendPacket(packet, callback);
648	}
649	
650	private synchronized void sendPacket(final AbstractStanza packet, PacketReceived callback) {
651		// TODO dont increment stanza count if packet = request packet or ack;
652		++stanzasSent;
653		tagWriter.writeStanzaAsync(packet);
654		if (callback != null) {
655			if (packet.getId()==null) {
656				packet.setId(nextRandomId());
657			}
658			packetCallbacks.put(packet.getId(), callback);
659		}
660	}
661	
662	public void sendPing() {
663		if (streamFeatures.hasChild("sm")) {
664			tagWriter.writeStanzaAsync(new RequestPacket());
665		} else {
666			IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
667			iq.setFrom(account.getFullJid());
668			iq.addChild("ping","urn:xmpp:ping");
669			this.sendIqPacket(iq, null);
670		}
671	}
672
673	public void setOnMessagePacketReceivedListener(
674			OnMessagePacketReceived listener) {
675		this.messageListener = listener;
676	}
677
678	public void setOnUnregisteredIqPacketReceivedListener(
679			OnIqPacketReceived listener) {
680		this.unregisteredIqListener = listener;
681	}
682
683	public void setOnPresencePacketReceivedListener(
684			OnPresencePacketReceived listener) {
685		this.presenceListener = listener;
686	}
687
688	public void setOnStatusChangedListener(OnStatusChanged listener) {
689		this.statusListener = listener;
690	}
691	
692	public void setOnTLSExceptionReceivedListener(OnTLSExceptionReceived listener) {
693		this.tlsListener = listener;
694	}
695	
696	public void setOnBindListener(OnBindListener listener) {
697		this.bindListener = listener;
698	}
699
700	public void disconnect(boolean force) {
701		changeStatus(Account.STATUS_OFFLINE);
702		Log.d(LOGTAG,"disconnecting");
703		try {
704		if (force) {
705				socket.close();
706				return;
707		}
708		tagWriter.finish();
709		while(!tagWriter.finished()) {
710			//Log.d(LOGTAG,"not yet finished");
711			Thread.sleep(100);
712		}
713		tagWriter.writeTag(Tag.end("stream:stream"));
714		} catch (IOException e) {
715			Log.d(LOGTAG,"io exception during disconnect");
716		} catch (InterruptedException e) {
717			Log.d(LOGTAG,"interupted while waiting for disconnect");
718		}
719	}
720	
721	public boolean hasFeatureRosterManagment() {
722		if (this.streamFeatures==null) {
723			return false;
724		} else {
725			return this.streamFeatures.hasChild("ver");
726		}
727	}
728	
729	public boolean hasFeatureStreamManagment() {
730		if (this.streamFeatures==null) {
731			return false;
732		} else {
733			return this.streamFeatures.hasChild("sm");
734		}
735	}
736	
737	public boolean hasFeaturesCarbon() {
738		return discoFeatures.contains("urn:xmpp:carbons:2");
739	}
740
741	public void r() {
742		this.tagWriter.writeStanzaAsync(new RequestPacket());
743	}
744
745	public int getReceivedStanzas() {
746		return this.stanzasReceived;
747	}
748	
749	public int getSentStanzas() {
750		return this.stanzasSent;
751	}
752
753	public String getMucServer() {
754		for(int i = 0; i < discoItems.size(); ++i) {
755			if (discoItems.get(i).contains("conference.")) {
756				return discoItems.get(i);
757			} else if (discoItems.get(i).contains("conf.")) {
758				return discoItems.get(i);
759			} else if (discoItems.get(i).contains("muc.")) {
760				return discoItems.get(i);
761			}
762		}
763		return null;
764	}
765}