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.HashMap;
20import java.util.Hashtable;
21import java.util.List;
22import java.util.Map.Entry;
23
24import javax.net.ssl.HostnameVerifier;
25import javax.net.ssl.SSLContext;
26import javax.net.ssl.SSLSocket;
27import javax.net.ssl.SSLSocketFactory;
28import javax.net.ssl.TrustManager;
29import javax.net.ssl.TrustManagerFactory;
30import javax.net.ssl.X509TrustManager;
31
32import org.bouncycastle.pqc.math.linearalgebra.GoppaCode.MaMaPe;
33import org.xmlpull.v1.XmlPullParserException;
34
35import de.duenndns.ssl.MemorizingTrustManager;
36
37import android.content.Context;
38import android.os.Bundle;
39import android.os.PowerManager;
40import android.os.PowerManager.WakeLock;
41import android.os.SystemClock;
42import android.util.Log;
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.ZLibOutputStream;
48import eu.siacs.conversations.utils.zlib.ZLibInputStream;
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.streammgmt.AckPacket;
60import eu.siacs.conversations.xmpp.stanzas.streammgmt.EnablePacket;
61import eu.siacs.conversations.xmpp.stanzas.streammgmt.RequestPacket;
62import eu.siacs.conversations.xmpp.stanzas.streammgmt.ResumePacket;
63
64public class XmppConnection implements Runnable {
65
66 protected Account account;
67 private static final String LOGTAG = "xmppService";
68
69 private WakeLock wakeLock;
70
71 private SecureRandom mRandom;
72
73 private Socket socket;
74 private XmlReader tagReader;
75 private TagWriter tagWriter;
76
77 private boolean shouldBind = true;
78 private boolean shouldAuthenticate = true;
79 private Element streamFeatures;
80 private HashMap<String, List<String>> disco = new HashMap<String, List<String>>();
81
82 private String streamId = null;
83 private int smVersion = 3;
84
85 private int stanzasReceived = 0;
86 private int stanzasSent = 0;
87
88 public long lastPaketReceived = 0;
89 public long lastPingSent = 0;
90 public long lastConnect = 0;
91 public long lastSessionStarted = 0;
92
93 private int attempt = 0;
94
95 private static final int PACKET_IQ = 0;
96 private static final int PACKET_MESSAGE = 1;
97 private static final int PACKET_PRESENCE = 2;
98
99 private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<String, PacketReceived>();
100 private OnPresencePacketReceived presenceListener = null;
101 private OnJinglePacketReceived jingleListener = null;
102 private OnIqPacketReceived unregisteredIqListener = null;
103 private OnMessagePacketReceived messageListener = null;
104 private OnStatusChanged statusListener = null;
105 private OnBindListener bindListener = null;
106 private MemorizingTrustManager mMemorizingTrustManager;
107
108 public XmppConnection(Account account, XmppConnectionService service) {
109 this.mRandom = service.getRNG();
110 this.mMemorizingTrustManager = service.getMemorizingTrustManager();
111 this.account = account;
112 this.wakeLock = service.getPowerManager().newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
113 account.getJid());
114 tagWriter = new TagWriter();
115 }
116
117 protected void changeStatus(int nextStatus) {
118 if (account.getStatus() != nextStatus) {
119 if ((nextStatus == Account.STATUS_OFFLINE)
120 && (account.getStatus() != Account.STATUS_CONNECTING)
121 && (account.getStatus() != Account.STATUS_ONLINE)
122 && (account.getStatus() != Account.STATUS_DISABLED)) {
123 return;
124 }
125 if (nextStatus == Account.STATUS_ONLINE) {
126 this.attempt = 0;
127 }
128 account.setStatus(nextStatus);
129 if (statusListener != null) {
130 statusListener.onStatusChanged(account);
131 }
132 }
133 }
134
135 protected void connect() {
136 Log.d(LOGTAG, account.getJid() + ": connecting");
137 lastConnect = SystemClock.elapsedRealtime();
138 this.attempt++;
139 try {
140 shouldAuthenticate = shouldBind = !account
141 .isOptionSet(Account.OPTION_REGISTER);
142 tagReader = new XmlReader(wakeLock);
143 tagWriter = new TagWriter();
144 packetCallbacks.clear();
145 this.changeStatus(Account.STATUS_CONNECTING);
146 Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
147 if ("timeout".equals(namePort.getString("error"))) {
148 Log.d(LOGTAG, account.getJid() + ": dns timeout");
149 this.changeStatus(Account.STATUS_OFFLINE);
150 return;
151 }
152 String srvRecordServer = namePort.getString("name");
153 String srvIpServer = namePort.getString("ipv4");
154 int srvRecordPort = namePort.getInt("port");
155 if (srvRecordServer != null) {
156 if (srvIpServer != null) {
157 Log.d(LOGTAG, account.getJid() + ": using values from dns "
158 + srvRecordServer + "[" + srvIpServer + "]:"
159 + srvRecordPort);
160 socket = new Socket(srvIpServer, srvRecordPort);
161 } else {
162 Log.d(LOGTAG, account.getJid() + ": using values from dns "
163 + srvRecordServer + ":" + srvRecordPort);
164 socket = new Socket(srvRecordServer, srvRecordPort);
165 }
166 } else {
167 socket = new Socket(account.getServer(), 5222);
168 }
169 OutputStream out = socket.getOutputStream();
170 tagWriter.setOutputStream(out);
171 InputStream in = socket.getInputStream();
172 tagReader.setInputStream(in);
173 tagWriter.beginDocument();
174 sendStartStream();
175 Tag nextTag;
176 while ((nextTag = tagReader.readTag()) != null) {
177 if (nextTag.isStart("stream")) {
178 processStream(nextTag);
179 break;
180 } else {
181 Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName());
182 return;
183 }
184 }
185 if (socket.isConnected()) {
186 socket.close();
187 }
188 } catch (UnknownHostException e) {
189 this.changeStatus(Account.STATUS_SERVER_NOT_FOUND);
190 if (wakeLock.isHeld()) {
191 try { wakeLock.release();} catch (RuntimeException re) {}
192 }
193 return;
194 } catch (IOException e) {
195 if (account.getStatus() != Account.STATUS_TLS_ERROR) {
196 this.changeStatus(Account.STATUS_OFFLINE);
197 }
198 if (wakeLock.isHeld()) {
199 try { wakeLock.release();} catch (RuntimeException re) {}
200 }
201 return;
202 } catch (NoSuchAlgorithmException e) {
203 this.changeStatus(Account.STATUS_OFFLINE);
204 Log.d(LOGTAG, "compression exception " + e.getMessage());
205 if (wakeLock.isHeld()) {
206 try { wakeLock.release();} catch (RuntimeException re) {}
207 }
208 return;
209 } catch (XmlPullParserException e) {
210 this.changeStatus(Account.STATUS_OFFLINE);
211 Log.d(LOGTAG, "xml exception " + e.getMessage());
212 if (wakeLock.isHeld()) {
213 try { wakeLock.release();} catch (RuntimeException re) {}
214 }
215 return;
216 }
217
218 }
219
220 @Override
221 public void run() {
222 connect();
223 }
224
225 private void processStream(Tag currentTag) throws XmlPullParserException,
226 IOException, NoSuchAlgorithmException {
227 Tag nextTag = tagReader.readTag();
228 while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
229 if (nextTag.isStart("error")) {
230 processStreamError(nextTag);
231 } else if (nextTag.isStart("features")) {
232 processStreamFeatures(nextTag);
233 if ((streamFeatures.getChildren().size() == 1)
234 && (streamFeatures.hasChild("starttls"))
235 && (!account.isOptionSet(Account.OPTION_USETLS))) {
236 changeStatus(Account.STATUS_SERVER_REQUIRES_TLS);
237 }
238 } else if (nextTag.isStart("proceed")) {
239 switchOverToTls(nextTag);
240 } else if (nextTag.isStart("compressed")) {
241 switchOverToZLib(nextTag);
242 } else if (nextTag.isStart("success")) {
243 Log.d(LOGTAG, account.getJid() + ": logged in");
244 tagReader.readTag();
245 tagReader.reset();
246 sendStartStream();
247 processStream(tagReader.readTag());
248 break;
249 } else if (nextTag.isStart("failure")) {
250 tagReader.readElement(nextTag);
251 changeStatus(Account.STATUS_UNAUTHORIZED);
252 } else if (nextTag.isStart("challenge")) {
253 String challange = tagReader.readElement(nextTag).getContent();
254 Element response = new Element("response");
255 response.setAttribute("xmlns",
256 "urn:ietf:params:xml:ns:xmpp-sasl");
257 response.setContent(CryptoHelper.saslDigestMd5(account,
258 challange,mRandom));
259 tagWriter.writeElement(response);
260 } else if (nextTag.isStart("enabled")) {
261 this.stanzasSent = 0;
262 Element enabled = tagReader.readElement(nextTag);
263 if ("true".equals(enabled.getAttribute("resume"))) {
264 this.streamId = enabled.getAttribute("id");
265 Log.d(LOGTAG, account.getJid() + ": stream managment("
266 + smVersion + ") enabled (resumable)");
267 } else {
268 Log.d(LOGTAG, account.getJid() + ": stream managment("
269 + smVersion + ") enabled");
270 }
271 this.lastSessionStarted = SystemClock.elapsedRealtime();
272 this.stanzasReceived = 0;
273 RequestPacket r = new RequestPacket(smVersion);
274 tagWriter.writeStanzaAsync(r);
275 } else if (nextTag.isStart("resumed")) {
276 lastPaketReceived = SystemClock.elapsedRealtime();
277 Log.d(LOGTAG, account.getJid() + ": session resumed");
278 tagReader.readElement(nextTag);
279 sendPing();
280 changeStatus(Account.STATUS_ONLINE);
281 } else if (nextTag.isStart("r")) {
282 tagReader.readElement(nextTag);
283 AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
284 tagWriter.writeStanzaAsync(ack);
285 } else if (nextTag.isStart("a")) {
286 Element ack = tagReader.readElement(nextTag);
287 lastPaketReceived = SystemClock.elapsedRealtime();
288 int serverSequence = Integer.parseInt(ack.getAttribute("h"));
289 if (serverSequence > this.stanzasSent) {
290 this.stanzasSent = serverSequence;
291 }
292 } else if (nextTag.isStart("failed")) {
293 tagReader.readElement(nextTag);
294 Log.d(LOGTAG, account.getJid() + ": resumption failed");
295 streamId = null;
296 if (account.getStatus() != Account.STATUS_ONLINE) {
297 sendBindRequest();
298 }
299 } else if (nextTag.isStart("iq")) {
300 processIq(nextTag);
301 } else if (nextTag.isStart("message")) {
302 processMessage(nextTag);
303 } else if (nextTag.isStart("presence")) {
304 processPresence(nextTag);
305 }
306 nextTag = tagReader.readTag();
307 }
308 if (account.getStatus() == Account.STATUS_ONLINE) {
309 account.setStatus(Account.STATUS_OFFLINE);
310 if (statusListener != null) {
311 statusListener.onStatusChanged(account);
312 }
313 }
314 }
315
316 private Element processPacket(Tag currentTag, int packetType)
317 throws XmlPullParserException, IOException {
318 Element element;
319 switch (packetType) {
320 case PACKET_IQ:
321 element = new IqPacket();
322 break;
323 case PACKET_MESSAGE:
324 element = new MessagePacket();
325 break;
326 case PACKET_PRESENCE:
327 element = new PresencePacket();
328 break;
329 default:
330 return null;
331 }
332 element.setAttributes(currentTag.getAttributes());
333 Tag nextTag = tagReader.readTag();
334 if (nextTag==null) {
335 throw new IOException("interrupted mid tag");
336 }
337 while (!nextTag.isEnd(element.getName())) {
338 if (!nextTag.isNo()) {
339 Element child = tagReader.readElement(nextTag);
340 if ((packetType == PACKET_IQ)
341 && ("jingle".equals(child.getName()))) {
342 element = new JinglePacket();
343 element.setAttributes(currentTag.getAttributes());
344 }
345 element.addChild(child);
346 }
347 nextTag = tagReader.readTag();
348 if (nextTag==null) {
349 throw new IOException("interrupted mid tag");
350 }
351 }
352 ++stanzasReceived;
353 lastPaketReceived = SystemClock.elapsedRealtime();
354 return element;
355 }
356
357 private void processIq(Tag currentTag) throws XmlPullParserException,
358 IOException {
359 IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
360
361 if (packet.getId() == null) {
362 return; // an iq packet without id is definitely invalid
363 }
364
365 if (packet instanceof JinglePacket) {
366 if (this.jingleListener != null) {
367 this.jingleListener.onJinglePacketReceived(account,
368 (JinglePacket) packet);
369 }
370 } else {
371 if (packetCallbacks.containsKey(packet.getId())) {
372 if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
373 ((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
374 .onIqPacketReceived(account, packet);
375 }
376
377 packetCallbacks.remove(packet.getId());
378 } else if (this.unregisteredIqListener != null) {
379 this.unregisteredIqListener.onIqPacketReceived(account, packet);
380 }
381 }
382 }
383
384 private void processMessage(Tag currentTag) throws XmlPullParserException,
385 IOException {
386 MessagePacket packet = (MessagePacket) processPacket(currentTag,
387 PACKET_MESSAGE);
388 String id = packet.getAttribute("id");
389 if ((id != null) && (packetCallbacks.containsKey(id))) {
390 if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
391 ((OnMessagePacketReceived) packetCallbacks.get(id))
392 .onMessagePacketReceived(account, packet);
393 }
394 packetCallbacks.remove(id);
395 } else if (this.messageListener != null) {
396 this.messageListener.onMessagePacketReceived(account, packet);
397 }
398 }
399
400 private void processPresence(Tag currentTag) throws XmlPullParserException,
401 IOException {
402 PresencePacket packet = (PresencePacket) processPacket(currentTag,
403 PACKET_PRESENCE);
404 String id = packet.getAttribute("id");
405 if ((id != null) && (packetCallbacks.containsKey(id))) {
406 if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
407 ((OnPresencePacketReceived) packetCallbacks.get(id))
408 .onPresencePacketReceived(account, packet);
409 }
410 packetCallbacks.remove(id);
411 } else if (this.presenceListener != null) {
412 this.presenceListener.onPresencePacketReceived(account, packet);
413 }
414 }
415
416 private void sendCompressionZlib() throws IOException {
417 Element compress = new Element("compress");
418 compress.setAttribute("xmlns", "http://jabber.org/protocol/compress");
419 compress.addChild("method").setContent("zlib");
420 tagWriter.writeElement(compress);
421 }
422
423 private void switchOverToZLib(Tag currentTag)
424 throws XmlPullParserException, IOException,
425 NoSuchAlgorithmException {
426 tagReader.readTag(); // read tag close
427
428 tagWriter.setOutputStream(new ZLibOutputStream(tagWriter
429 .getOutputStream()));
430 tagReader
431 .setInputStream(new ZLibInputStream(tagReader.getInputStream()));
432
433 sendStartStream();
434 Log.d(LOGTAG, account.getJid() + ": compression enabled");
435 processStream(tagReader.readTag());
436 }
437
438 private void sendStartTLS() throws IOException {
439 Tag startTLS = Tag.empty("starttls");
440 startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
441 tagWriter.writeTag(startTLS);
442 }
443
444 private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
445 IOException {
446 tagReader.readTag();
447 try {
448 SSLContext sc = SSLContext.getInstance("TLS");
449 sc.init(null, new X509TrustManager[] { this.mMemorizingTrustManager }, mRandom);
450 SSLSocketFactory factory = sc.getSocketFactory();
451
452 HostnameVerifier verifier = this.mMemorizingTrustManager.wrapHostnameVerifier(new org.apache.http.conn.ssl.StrictHostnameVerifier());
453 SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
454 socket.getInetAddress().getHostAddress(), socket.getPort(),
455 true);
456
457 if (verifier != null && !verifier.verify(account.getServer(), sslSocket.getSession())) {
458 Log.d(LOGTAG, account.getJid() + ": host mismatch in TLS connection");
459 sslSocket.close();
460 throw new IOException();
461 }
462 tagReader.setInputStream(sslSocket.getInputStream());
463 tagWriter.setOutputStream(sslSocket.getOutputStream());
464 sendStartStream();
465 Log.d(LOGTAG, account.getJid() + ": TLS connection established");
466 processStream(tagReader.readTag());
467 sslSocket.close();
468 } catch (NoSuchAlgorithmException e1) {
469 e1.printStackTrace();
470 } catch (KeyManagementException e) {
471 e.printStackTrace();
472 }
473 }
474
475 private void sendSaslAuthPlain() throws IOException {
476 String saslString = CryptoHelper.saslPlain(account.getUsername(),
477 account.getPassword());
478 Element auth = new Element("auth");
479 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
480 auth.setAttribute("mechanism", "PLAIN");
481 auth.setContent(saslString);
482 tagWriter.writeElement(auth);
483 }
484
485 private void sendSaslAuthDigestMd5() throws IOException {
486 Element auth = new Element("auth");
487 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
488 auth.setAttribute("mechanism", "DIGEST-MD5");
489 tagWriter.writeElement(auth);
490 }
491
492 private void processStreamFeatures(Tag currentTag)
493 throws XmlPullParserException, IOException {
494 this.streamFeatures = tagReader.readElement(currentTag);
495 if (this.streamFeatures.hasChild("starttls")
496 && account.isOptionSet(Account.OPTION_USETLS)) {
497 sendStartTLS();
498 } else if (compressionAvailable()) {
499 sendCompressionZlib();
500 } else if (this.streamFeatures.hasChild("register")
501 && (account.isOptionSet(Account.OPTION_REGISTER))) {
502 sendRegistryRequest();
503 } else if (!this.streamFeatures.hasChild("register")
504 && (account.isOptionSet(Account.OPTION_REGISTER))) {
505 changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
506 disconnect(true);
507 } else if (this.streamFeatures.hasChild("mechanisms")
508 && shouldAuthenticate) {
509 List<String> mechanisms = extractMechanisms(streamFeatures
510 .findChild("mechanisms"));
511 if (mechanisms.contains("PLAIN")) {
512 sendSaslAuthPlain();
513 } else if (mechanisms.contains("DIGEST-MD5")) {
514 sendSaslAuthDigestMd5();
515 }
516 } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
517 + smVersion)
518 && streamId != null) {
519 ResumePacket resume = new ResumePacket(this.streamId,
520 stanzasReceived, smVersion);
521 this.tagWriter.writeStanzaAsync(resume);
522 } else if (this.streamFeatures.hasChild("bind") && shouldBind) {
523 sendBindRequest();
524 }
525 }
526
527 private boolean compressionAvailable() {
528 if (!this.streamFeatures.hasChild("compression",
529 "http://jabber.org/features/compress"))
530 return false;
531 if (!ZLibOutputStream.SUPPORTED)
532 return false;
533 if (!account.isOptionSet(Account.OPTION_USECOMPRESSION))
534 return false;
535
536 Element compression = this.streamFeatures.findChild("compression",
537 "http://jabber.org/features/compress");
538 for (Element child : compression.getChildren()) {
539 if (!"method".equals(child.getName()))
540 continue;
541
542 if ("zlib".equalsIgnoreCase(child.getContent())) {
543 return true;
544 }
545 }
546 return false;
547 }
548
549 private List<String> extractMechanisms(Element stream) {
550 ArrayList<String> mechanisms = new ArrayList<String>(stream
551 .getChildren().size());
552 for (Element child : stream.getChildren()) {
553 mechanisms.add(child.getContent());
554 }
555 return mechanisms;
556 }
557
558 private void sendRegistryRequest() {
559 IqPacket register = new IqPacket(IqPacket.TYPE_GET);
560 register.query("jabber:iq:register");
561 register.setTo(account.getServer());
562 sendIqPacket(register, new OnIqPacketReceived() {
563
564 @Override
565 public void onIqPacketReceived(Account account, IqPacket packet) {
566 Element instructions = packet.query().findChild("instructions");
567 if (packet.query().hasChild("username")
568 && (packet.query().hasChild("password"))) {
569 IqPacket register = new IqPacket(IqPacket.TYPE_SET);
570 Element username = new Element("username")
571 .setContent(account.getUsername());
572 Element password = new Element("password")
573 .setContent(account.getPassword());
574 register.query("jabber:iq:register").addChild(username);
575 register.query().addChild(password);
576 sendIqPacket(register, new OnIqPacketReceived() {
577
578 @Override
579 public void onIqPacketReceived(Account account,
580 IqPacket packet) {
581 if (packet.getType() == IqPacket.TYPE_RESULT) {
582 account.setOption(Account.OPTION_REGISTER,
583 false);
584 changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
585 } else if (packet.hasChild("error")
586 && (packet.findChild("error")
587 .hasChild("conflict"))) {
588 changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
589 } else {
590 changeStatus(Account.STATUS_REGISTRATION_FAILED);
591 Log.d(LOGTAG, packet.toString());
592 }
593 disconnect(true);
594 }
595 });
596 } else {
597 changeStatus(Account.STATUS_REGISTRATION_FAILED);
598 disconnect(true);
599 Log.d(LOGTAG, account.getJid()
600 + ": could not register. instructions are"
601 + instructions.getContent());
602 }
603 }
604 });
605 }
606
607 private void sendBindRequest() throws IOException {
608 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
609 iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
610 .addChild("resource").setContent(account.getResource());
611 this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
612 @Override
613 public void onIqPacketReceived(Account account, IqPacket packet) {
614 String resource = packet.findChild("bind").findChild("jid")
615 .getContent().split("/")[1];
616 account.setResource(resource);
617 if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
618 smVersion = 3;
619 EnablePacket enable = new EnablePacket(smVersion);
620 tagWriter.writeStanzaAsync(enable);
621 } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) {
622 smVersion = 2;
623 EnablePacket enable = new EnablePacket(smVersion);
624 tagWriter.writeStanzaAsync(enable);
625 }
626 sendServiceDiscoveryInfo(account.getServer());
627 sendServiceDiscoveryItems(account.getServer());
628 if (bindListener != null) {
629 bindListener.onBind(account);
630 }
631
632 changeStatus(Account.STATUS_ONLINE);
633 }
634 });
635 if (this.streamFeatures.hasChild("session")) {
636 Log.d(LOGTAG, account.getJid() + ": sending deprecated session");
637 IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
638 startSession.addChild("session",
639 "urn:ietf:params:xml:ns:xmpp-session");
640 this.sendUnboundIqPacket(startSession, null);
641 }
642 }
643
644 private void sendServiceDiscoveryInfo(final String server) {
645 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
646 iq.setTo(server);
647 iq.query("http://jabber.org/protocol/disco#info");
648 this.sendIqPacket(iq, new OnIqPacketReceived() {
649
650 @Override
651 public void onIqPacketReceived(Account account, IqPacket packet) {
652 List<Element> elements = packet.query().getChildren();
653 List<String> features = new ArrayList<String>();
654 for (int i = 0; i < elements.size(); ++i) {
655 if (elements.get(i).getName().equals("feature")) {
656 features.add(elements.get(i).getAttribute("var"));
657 }
658 }
659 disco.put(server, features);
660
661 if (account.getServer().equals(server)) {
662 enableAdvancedStreamFeatures();
663 }
664 }
665 });
666 }
667
668 private void enableAdvancedStreamFeatures() {
669 if (hasFeaturesCarbon()) {
670 sendEnableCarbons();
671 }
672 }
673
674 private void sendServiceDiscoveryItems(final String server) {
675 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
676 iq.setTo(server);
677 iq.query("http://jabber.org/protocol/disco#items");
678 this.sendIqPacket(iq, new OnIqPacketReceived() {
679
680 @Override
681 public void onIqPacketReceived(Account account, IqPacket packet) {
682 List<Element> elements = packet.query().getChildren();
683 for (int i = 0; i < elements.size(); ++i) {
684 if (elements.get(i).getName().equals("item")) {
685 String jid = elements.get(i).getAttribute("jid");
686 sendServiceDiscoveryInfo(jid);
687 }
688 }
689 }
690 });
691 }
692
693 private void sendEnableCarbons() {
694 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
695 iq.addChild("enable", "urn:xmpp:carbons:2");
696 this.sendIqPacket(iq, new OnIqPacketReceived() {
697
698 @Override
699 public void onIqPacketReceived(Account account, IqPacket packet) {
700 if (!packet.hasChild("error")) {
701 Log.d(LOGTAG, account.getJid()
702 + ": successfully enabled carbons");
703 } else {
704 Log.d(LOGTAG, account.getJid()
705 + ": error enableing carbons " + packet.toString());
706 }
707 }
708 });
709 }
710
711 private void processStreamError(Tag currentTag) {
712 Log.d(LOGTAG, "processStreamError");
713 }
714
715 private void sendStartStream() throws IOException {
716 Tag stream = Tag.start("stream:stream");
717 stream.setAttribute("from", account.getJid());
718 stream.setAttribute("to", account.getServer());
719 stream.setAttribute("version", "1.0");
720 stream.setAttribute("xml:lang", "en");
721 stream.setAttribute("xmlns", "jabber:client");
722 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
723 tagWriter.writeTag(stream);
724 }
725
726 private String nextRandomId() {
727 return new BigInteger(50, mRandom).toString(32);
728 }
729
730 public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
731 if (packet.getId() == null) {
732 String id = nextRandomId();
733 packet.setAttribute("id", id);
734 }
735 packet.setFrom(account.getFullJid());
736 this.sendPacket(packet, callback);
737 }
738
739 public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
740 if (packet.getId() == null) {
741 String id = nextRandomId();
742 packet.setAttribute("id", id);
743 }
744 this.sendPacket(packet, callback);
745 }
746
747 public void sendMessagePacket(MessagePacket packet) {
748 this.sendPacket(packet, null);
749 }
750
751 public void sendPresencePacket(PresencePacket packet) {
752 this.sendPacket(packet, null);
753 }
754
755 private synchronized void sendPacket(final AbstractStanza packet,
756 PacketReceived callback) {
757 // TODO dont increment stanza count if packet = request packet or ack;
758 ++stanzasSent;
759 tagWriter.writeStanzaAsync(packet);
760 if (callback != null) {
761 if (packet.getId() == null) {
762 packet.setId(nextRandomId());
763 }
764 packetCallbacks.put(packet.getId(), callback);
765 }
766 }
767
768 public void sendPing() {
769 if (streamFeatures.hasChild("sm")) {
770 tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
771 } else {
772 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
773 iq.setFrom(account.getFullJid());
774 iq.addChild("ping", "urn:xmpp:ping");
775 this.sendIqPacket(iq, null);
776 }
777 }
778
779 public void setOnMessagePacketReceivedListener(
780 OnMessagePacketReceived listener) {
781 this.messageListener = listener;
782 }
783
784 public void setOnUnregisteredIqPacketReceivedListener(
785 OnIqPacketReceived listener) {
786 this.unregisteredIqListener = listener;
787 }
788
789 public void setOnPresencePacketReceivedListener(
790 OnPresencePacketReceived listener) {
791 this.presenceListener = listener;
792 }
793
794 public void setOnJinglePacketReceivedListener(
795 OnJinglePacketReceived listener) {
796 this.jingleListener = listener;
797 }
798
799 public void setOnStatusChangedListener(OnStatusChanged listener) {
800 this.statusListener = listener;
801 }
802
803 public void setOnBindListener(OnBindListener listener) {
804 this.bindListener = listener;
805 }
806
807 public void disconnect(boolean force) {
808 changeStatus(Account.STATUS_OFFLINE);
809 Log.d(LOGTAG, "disconnecting");
810 try {
811 if (force) {
812 socket.close();
813 return;
814 }
815 new Thread(new Runnable() {
816
817 @Override
818 public void run() {
819 if (tagWriter.isActive()) {
820 tagWriter.finish();
821 try {
822 while (!tagWriter.finished()) {
823 Log.d(LOGTAG, "not yet finished");
824 Thread.sleep(100);
825 }
826 tagWriter.writeTag(Tag.end("stream:stream"));
827 } catch (IOException e) {
828 Log.d(LOGTAG, "io exception during disconnect");
829 } catch (InterruptedException e) {
830 Log.d(LOGTAG, "interrupted");
831 }
832 }
833 }
834 }).start();
835 } catch (IOException e) {
836 Log.d(LOGTAG, "io exception during disconnect");
837 }
838 }
839
840 public boolean hasFeatureRosterManagment() {
841 if (this.streamFeatures == null) {
842 return false;
843 } else {
844 return this.streamFeatures.hasChild("ver");
845 }
846 }
847
848 public boolean hasFeatureStreamManagment() {
849 if (this.streamFeatures == null) {
850 return false;
851 } else {
852 return this.streamFeatures.hasChild("sm");
853 }
854 }
855
856 public boolean hasFeaturesCarbon() {
857 return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
858 }
859
860 public boolean hasDiscoFeature(String server, String feature) {
861 if (!disco.containsKey(server)) {
862 return false;
863 }
864 return disco.get(server).contains(feature);
865 }
866
867 public List<String> findDiscoItemsByFeature(String feature) {
868 List<String> items = new ArrayList<String>();
869 for (Entry<String, List<String>> cursor : disco.entrySet()) {
870 if (cursor.getValue().contains(feature)) {
871 items.add(cursor.getKey());
872 }
873 }
874 return items;
875 }
876
877 public String findDiscoItemByFeature(String feature) {
878 List<String> items = findDiscoItemsByFeature(feature);
879 if (items.size()>=1) {
880 return items.get(0);
881 }
882 return null;
883 }
884
885 public void r() {
886 this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
887 }
888
889 public int getReceivedStanzas() {
890 return this.stanzasReceived;
891 }
892
893 public int getSentStanzas() {
894 return this.stanzasSent;
895 }
896
897 public String getMucServer() {
898 return findDiscoItemByFeature("http://jabber.org/protocol/muc");
899 }
900
901 public int getTimeToNextAttempt() {
902 int interval = (int) (25 * Math.pow(1.5, attempt));
903 int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
904 return interval - secondsSinceLast;
905 }
906
907 public int getAttempt() {
908 return this.attempt;
909 }
910}