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