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 sendInitialPing();
315
316 } else if (nextTag.isStart("r")) {
317 tagReader.readElement(nextTag);
318 AckPacket ack = new AckPacket(this.stanzasReceived, smVersion);
319 tagWriter.writeStanzaAsync(ack);
320 } else if (nextTag.isStart("a")) {
321 Element ack = tagReader.readElement(nextTag);
322 lastPaketReceived = SystemClock.elapsedRealtime();
323 int serverSequence = Integer.parseInt(ack.getAttribute("h"));
324 String msgId = this.messageReceipts.get(serverSequence);
325 if (msgId != null) {
326 if (this.acknowledgedListener != null) {
327 this.acknowledgedListener.onMessageAcknowledged(
328 account, msgId);
329 }
330 this.messageReceipts.remove(serverSequence);
331 }
332 } else if (nextTag.isStart("failed")) {
333 tagReader.readElement(nextTag);
334 Log.d(Config.LOGTAG, account.getJid() + ": resumption failed");
335 streamId = null;
336 if (account.getStatus() != Account.STATUS_ONLINE) {
337 sendBindRequest();
338 }
339 } else if (nextTag.isStart("iq")) {
340 processIq(nextTag);
341 } else if (nextTag.isStart("message")) {
342 processMessage(nextTag);
343 } else if (nextTag.isStart("presence")) {
344 processPresence(nextTag);
345 }
346 nextTag = tagReader.readTag();
347 }
348 if (account.getStatus() == Account.STATUS_ONLINE) {
349 account.setStatus(Account.STATUS_OFFLINE);
350 if (statusListener != null) {
351 statusListener.onStatusChanged(account);
352 }
353 }
354 }
355
356 private void sendInitialPing() {
357 Log.d(Config.LOGTAG,account.getJid()+": sending intial ping");
358 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
359 iq.setFrom(account.getFullJid());
360 iq.addChild("ping", "urn:xmpp:ping");
361 this.sendIqPacket(iq, new OnIqPacketReceived() {
362
363 @Override
364 public void onIqPacketReceived(Account account, IqPacket packet) {
365 Log.d(Config.LOGTAG,account.getJid()+": online with resource "+account.getResource());
366 changeStatus(Account.STATUS_ONLINE);
367 }
368 });
369 }
370
371 private Element processPacket(Tag currentTag, int packetType)
372 throws XmlPullParserException, IOException {
373 Element element;
374 switch (packetType) {
375 case PACKET_IQ:
376 element = new IqPacket();
377 break;
378 case PACKET_MESSAGE:
379 element = new MessagePacket();
380 break;
381 case PACKET_PRESENCE:
382 element = new PresencePacket();
383 break;
384 default:
385 return null;
386 }
387 element.setAttributes(currentTag.getAttributes());
388 Tag nextTag = tagReader.readTag();
389 if (nextTag == null) {
390 throw new IOException("interrupted mid tag");
391 }
392 while (!nextTag.isEnd(element.getName())) {
393 if (!nextTag.isNo()) {
394 Element child = tagReader.readElement(nextTag);
395 if ((packetType == PACKET_IQ)
396 && ("jingle".equals(child.getName()))) {
397 element = new JinglePacket();
398 element.setAttributes(currentTag.getAttributes());
399 }
400 element.addChild(child);
401 }
402 nextTag = tagReader.readTag();
403 if (nextTag == null) {
404 throw new IOException("interrupted mid tag");
405 }
406 }
407 ++stanzasReceived;
408 lastPaketReceived = SystemClock.elapsedRealtime();
409 return element;
410 }
411
412 private void processIq(Tag currentTag) throws XmlPullParserException,
413 IOException {
414 IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
415
416 if (packet.getId() == null) {
417 return; // an iq packet without id is definitely invalid
418 }
419
420 if (packet instanceof JinglePacket) {
421 if (this.jingleListener != null) {
422 this.jingleListener.onJinglePacketReceived(account,
423 (JinglePacket) packet);
424 }
425 } else {
426 if (packetCallbacks.containsKey(packet.getId())) {
427 if (packetCallbacks.get(packet.getId()) instanceof OnIqPacketReceived) {
428 ((OnIqPacketReceived) packetCallbacks.get(packet.getId()))
429 .onIqPacketReceived(account, packet);
430 }
431
432 packetCallbacks.remove(packet.getId());
433 } else if (this.unregisteredIqListener != null) {
434 this.unregisteredIqListener.onIqPacketReceived(account, packet);
435 }
436 }
437 }
438
439 private void processMessage(Tag currentTag) throws XmlPullParserException,
440 IOException {
441 MessagePacket packet = (MessagePacket) processPacket(currentTag,
442 PACKET_MESSAGE);
443 String id = packet.getAttribute("id");
444 if ((id != null) && (packetCallbacks.containsKey(id))) {
445 if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) {
446 ((OnMessagePacketReceived) packetCallbacks.get(id))
447 .onMessagePacketReceived(account, packet);
448 }
449 packetCallbacks.remove(id);
450 } else if (this.messageListener != null) {
451 this.messageListener.onMessagePacketReceived(account, packet);
452 }
453 }
454
455 private void processPresence(Tag currentTag) throws XmlPullParserException,
456 IOException {
457 PresencePacket packet = (PresencePacket) processPacket(currentTag,
458 PACKET_PRESENCE);
459 String id = packet.getAttribute("id");
460 if ((id != null) && (packetCallbacks.containsKey(id))) {
461 if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) {
462 ((OnPresencePacketReceived) packetCallbacks.get(id))
463 .onPresencePacketReceived(account, packet);
464 }
465 packetCallbacks.remove(id);
466 } else if (this.presenceListener != null) {
467 this.presenceListener.onPresencePacketReceived(account, packet);
468 }
469 }
470
471 private void sendCompressionZlib() throws IOException {
472 Element compress = new Element("compress");
473 compress.setAttribute("xmlns", "http://jabber.org/protocol/compress");
474 compress.addChild("method").setContent("zlib");
475 tagWriter.writeElement(compress);
476 }
477
478 private void switchOverToZLib(Tag currentTag)
479 throws XmlPullParserException, IOException,
480 NoSuchAlgorithmException {
481 tagReader.readTag(); // read tag close
482 tagWriter.setOutputStream(new ZLibOutputStream(tagWriter
483 .getOutputStream()));
484 tagReader
485 .setInputStream(new ZLibInputStream(tagReader.getInputStream()));
486
487 sendStartStream();
488 Log.d(Config.LOGTAG, account.getJid() + ": compression enabled");
489 usingCompression = true;
490 processStream(tagReader.readTag());
491 }
492
493 private void sendStartTLS() throws IOException {
494 Tag startTLS = Tag.empty("starttls");
495 startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
496 tagWriter.writeTag(startTLS);
497 }
498
499 private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
500 IOException {
501 tagReader.readTag();
502 try {
503 SSLContext sc = SSLContext.getInstance("TLS");
504 sc.init(null,
505 new X509TrustManager[] { this.mMemorizingTrustManager },
506 mRandom);
507 SSLSocketFactory factory = sc.getSocketFactory();
508
509 HostnameVerifier verifier = this.mMemorizingTrustManager
510 .wrapHostnameVerifier(new org.apache.http.conn.ssl.StrictHostnameVerifier());
511 SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
512 socket.getInetAddress().getHostAddress(), socket.getPort(),
513 true);
514
515 if (verifier != null
516 && !verifier.verify(account.getServer(),
517 sslSocket.getSession())) {
518 Log.d(Config.LOGTAG, account.getJid()
519 + ": host mismatch in TLS connection");
520 sslSocket.close();
521 throw new IOException();
522 }
523 tagReader.setInputStream(sslSocket.getInputStream());
524 tagWriter.setOutputStream(sslSocket.getOutputStream());
525 sendStartStream();
526 Log.d(Config.LOGTAG, account.getJid()
527 + ": TLS connection established");
528 processStream(tagReader.readTag());
529 sslSocket.close();
530 } catch (NoSuchAlgorithmException e1) {
531 e1.printStackTrace();
532 } catch (KeyManagementException e) {
533 e.printStackTrace();
534 }
535 }
536
537 private void sendSaslAuthPlain() throws IOException {
538 String saslString = CryptoHelper.saslPlain(account.getUsername(),
539 account.getPassword());
540 Element auth = new Element("auth");
541 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
542 auth.setAttribute("mechanism", "PLAIN");
543 auth.setContent(saslString);
544 tagWriter.writeElement(auth);
545 }
546
547 private void sendSaslAuthDigestMd5() throws IOException {
548 Element auth = new Element("auth");
549 auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
550 auth.setAttribute("mechanism", "DIGEST-MD5");
551 tagWriter.writeElement(auth);
552 }
553
554 private void processStreamFeatures(Tag currentTag)
555 throws XmlPullParserException, IOException {
556 this.streamFeatures = tagReader.readElement(currentTag);
557 if (this.streamFeatures.hasChild("starttls")
558 && account.isOptionSet(Account.OPTION_USETLS)) {
559 sendStartTLS();
560 } else if (compressionAvailable()) {
561 sendCompressionZlib();
562 } else if (this.streamFeatures.hasChild("register")
563 && (account.isOptionSet(Account.OPTION_REGISTER))) {
564 sendRegistryRequest();
565 } else if (!this.streamFeatures.hasChild("register")
566 && (account.isOptionSet(Account.OPTION_REGISTER))) {
567 changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
568 disconnect(true);
569 } else if (this.streamFeatures.hasChild("mechanisms")
570 && shouldAuthenticate) {
571 List<String> mechanisms = extractMechanisms(streamFeatures
572 .findChild("mechanisms"));
573 if (mechanisms.contains("PLAIN")) {
574 sendSaslAuthPlain();
575 } else if (mechanisms.contains("DIGEST-MD5")) {
576 sendSaslAuthDigestMd5();
577 }
578 } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:"
579 + smVersion)
580 && streamId != null) {
581 ResumePacket resume = new ResumePacket(this.streamId,
582 stanzasReceived, smVersion);
583 this.tagWriter.writeStanzaAsync(resume);
584 } else if (this.streamFeatures.hasChild("bind") && shouldBind) {
585 sendBindRequest();
586 }
587 }
588
589 private boolean compressionAvailable() {
590 if (!this.streamFeatures.hasChild("compression",
591 "http://jabber.org/features/compress"))
592 return false;
593 if (!ZLibOutputStream.SUPPORTED)
594 return false;
595 if (!account.isOptionSet(Account.OPTION_USECOMPRESSION))
596 return false;
597
598 Element compression = this.streamFeatures.findChild("compression",
599 "http://jabber.org/features/compress");
600 for (Element child : compression.getChildren()) {
601 if (!"method".equals(child.getName()))
602 continue;
603
604 if ("zlib".equalsIgnoreCase(child.getContent())) {
605 return true;
606 }
607 }
608 return false;
609 }
610
611 private List<String> extractMechanisms(Element stream) {
612 ArrayList<String> mechanisms = new ArrayList<String>(stream
613 .getChildren().size());
614 for (Element child : stream.getChildren()) {
615 mechanisms.add(child.getContent());
616 }
617 return mechanisms;
618 }
619
620 private void sendRegistryRequest() {
621 IqPacket register = new IqPacket(IqPacket.TYPE_GET);
622 register.query("jabber:iq:register");
623 register.setTo(account.getServer());
624 sendIqPacket(register, new OnIqPacketReceived() {
625
626 @Override
627 public void onIqPacketReceived(Account account, IqPacket packet) {
628 Element instructions = packet.query().findChild("instructions");
629 if (packet.query().hasChild("username")
630 && (packet.query().hasChild("password"))) {
631 IqPacket register = new IqPacket(IqPacket.TYPE_SET);
632 Element username = new Element("username")
633 .setContent(account.getUsername());
634 Element password = new Element("password")
635 .setContent(account.getPassword());
636 register.query("jabber:iq:register").addChild(username);
637 register.query().addChild(password);
638 sendIqPacket(register, new OnIqPacketReceived() {
639
640 @Override
641 public void onIqPacketReceived(Account account,
642 IqPacket packet) {
643 if (packet.getType() == IqPacket.TYPE_RESULT) {
644 account.setOption(Account.OPTION_REGISTER,
645 false);
646 changeStatus(Account.STATUS_REGISTRATION_SUCCESSFULL);
647 } else if (packet.hasChild("error")
648 && (packet.findChild("error")
649 .hasChild("conflict"))) {
650 changeStatus(Account.STATUS_REGISTRATION_CONFLICT);
651 } else {
652 changeStatus(Account.STATUS_REGISTRATION_FAILED);
653 Log.d(Config.LOGTAG, packet.toString());
654 }
655 disconnect(true);
656 }
657 });
658 } else {
659 changeStatus(Account.STATUS_REGISTRATION_FAILED);
660 disconnect(true);
661 Log.d(Config.LOGTAG, account.getJid()
662 + ": could not register. instructions are"
663 + instructions.getContent());
664 }
665 }
666 });
667 }
668
669 private void sendBindRequest() throws IOException {
670 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
671 iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
672 .addChild("resource").setContent(account.getResource());
673 this.sendUnboundIqPacket(iq, new OnIqPacketReceived() {
674 @Override
675 public void onIqPacketReceived(Account account, IqPacket packet) {
676 Element bind = packet.findChild("bind");
677 if (bind != null) {
678 Element jid = bind.findChild("jid");
679 if (jid != null && jid.getContent() != null) {
680 account.setResource(jid.getContent().split("/", 2)[1]);
681 if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) {
682 smVersion = 3;
683 EnablePacket enable = new EnablePacket(smVersion);
684 tagWriter.writeStanzaAsync(enable);
685 stanzasSent = 0;
686 messageReceipts.clear();
687 } else if (streamFeatures.hasChild("sm",
688 "urn:xmpp:sm:2")) {
689 smVersion = 2;
690 EnablePacket enable = new EnablePacket(smVersion);
691 tagWriter.writeStanzaAsync(enable);
692 stanzasSent = 0;
693 messageReceipts.clear();
694 }
695 sendServiceDiscoveryInfo(account.getServer());
696 sendServiceDiscoveryItems(account.getServer());
697 if (bindListener != null) {
698 bindListener.onBind(account);
699 }
700 sendInitialPing();
701 } else {
702 disconnect(true);
703 }
704 } else {
705 disconnect(true);
706 }
707 }
708 });
709 if (this.streamFeatures.hasChild("session")) {
710 Log.d(Config.LOGTAG, account.getJid()
711 + ": sending deprecated session");
712 IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
713 startSession.addChild("session",
714 "urn:ietf:params:xml:ns:xmpp-session");
715 this.sendUnboundIqPacket(startSession, null);
716 }
717 }
718
719 private void sendServiceDiscoveryInfo(final String server) {
720 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
721 iq.setTo(server);
722 iq.query("http://jabber.org/protocol/disco#info");
723 this.sendIqPacket(iq, new OnIqPacketReceived() {
724
725 @Override
726 public void onIqPacketReceived(Account account, IqPacket packet) {
727 List<Element> elements = packet.query().getChildren();
728 List<String> features = new ArrayList<String>();
729 for (int i = 0; i < elements.size(); ++i) {
730 if (elements.get(i).getName().equals("feature")) {
731 features.add(elements.get(i).getAttribute("var"));
732 }
733 }
734 disco.put(server, features);
735
736 if (account.getServer().equals(server)) {
737 enableAdvancedStreamFeatures();
738 }
739 }
740 });
741 }
742
743 private void enableAdvancedStreamFeatures() {
744 if (getFeatures().carbons()) {
745 sendEnableCarbons();
746 }
747 }
748
749 private void sendServiceDiscoveryItems(final String server) {
750 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
751 iq.setTo(server);
752 iq.query("http://jabber.org/protocol/disco#items");
753 this.sendIqPacket(iq, new OnIqPacketReceived() {
754
755 @Override
756 public void onIqPacketReceived(Account account, IqPacket packet) {
757 List<Element> elements = packet.query().getChildren();
758 for (int i = 0; i < elements.size(); ++i) {
759 if (elements.get(i).getName().equals("item")) {
760 String jid = elements.get(i).getAttribute("jid");
761 sendServiceDiscoveryInfo(jid);
762 }
763 }
764 }
765 });
766 }
767
768 private void sendEnableCarbons() {
769 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
770 iq.addChild("enable", "urn:xmpp:carbons:2");
771 this.sendIqPacket(iq, new OnIqPacketReceived() {
772
773 @Override
774 public void onIqPacketReceived(Account account, IqPacket packet) {
775 if (!packet.hasChild("error")) {
776 Log.d(Config.LOGTAG, account.getJid()
777 + ": successfully enabled carbons");
778 } else {
779 Log.d(Config.LOGTAG, account.getJid()
780 + ": error enableing carbons " + packet.toString());
781 }
782 }
783 });
784 }
785
786 private void processStreamError(Tag currentTag)
787 throws XmlPullParserException, IOException {
788 Element streamError = tagReader.readElement(currentTag);
789 if (streamError != null && streamError.hasChild("conflict")) {
790 String resource = account.getResource().split("\\.")[0];
791 account.setResource(resource + "." + nextRandomId());
792 Log.d(Config.LOGTAG,
793 account.getJid() + ": switching resource due to conflict ("
794 + account.getResource() + ")");
795 }
796 }
797
798 private void sendStartStream() throws IOException {
799 Tag stream = Tag.start("stream:stream");
800 stream.setAttribute("from", account.getJid());
801 stream.setAttribute("to", account.getServer());
802 stream.setAttribute("version", "1.0");
803 stream.setAttribute("xml:lang", "en");
804 stream.setAttribute("xmlns", "jabber:client");
805 stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
806 tagWriter.writeTag(stream);
807 }
808
809 private String nextRandomId() {
810 return new BigInteger(50, mRandom).toString(32);
811 }
812
813 public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
814 if (packet.getId() == null) {
815 String id = nextRandomId();
816 packet.setAttribute("id", id);
817 }
818 packet.setFrom(account.getFullJid());
819 this.sendPacket(packet, callback);
820 }
821
822 public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) {
823 if (packet.getId() == null) {
824 String id = nextRandomId();
825 packet.setAttribute("id", id);
826 }
827 this.sendPacket(packet, callback);
828 }
829
830 public void sendMessagePacket(MessagePacket packet) {
831 this.sendPacket(packet, null);
832 }
833
834 public void sendPresencePacket(PresencePacket packet) {
835 this.sendPacket(packet, null);
836 }
837
838 private synchronized void sendPacket(final AbstractStanza packet,
839 PacketReceived callback) {
840 if (packet.getName().equals("iq") || packet.getName().equals("message")
841 || packet.getName().equals("presence")) {
842 ++stanzasSent;
843 }
844 tagWriter.writeStanzaAsync(packet);
845 if (packet instanceof MessagePacket && packet.getId() != null
846 && this.streamId != null) {
847 Log.d(Config.LOGTAG, "request delivery report for stanza "
848 + stanzasSent);
849 this.messageReceipts.put(stanzasSent, packet.getId());
850 tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
851 }
852 if (callback != null) {
853 if (packet.getId() == null) {
854 packet.setId(nextRandomId());
855 }
856 packetCallbacks.put(packet.getId(), callback);
857 }
858 }
859
860 public void sendPing() {
861 if (streamFeatures.hasChild("sm")) {
862 tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
863 } else {
864 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
865 iq.setFrom(account.getFullJid());
866 iq.addChild("ping", "urn:xmpp:ping");
867 this.sendIqPacket(iq, null);
868 }
869 this.lastPingSent = SystemClock.elapsedRealtime();
870 }
871
872 public void setOnMessagePacketReceivedListener(
873 OnMessagePacketReceived listener) {
874 this.messageListener = listener;
875 }
876
877 public void setOnUnregisteredIqPacketReceivedListener(
878 OnIqPacketReceived listener) {
879 this.unregisteredIqListener = listener;
880 }
881
882 public void setOnPresencePacketReceivedListener(
883 OnPresencePacketReceived listener) {
884 this.presenceListener = listener;
885 }
886
887 public void setOnJinglePacketReceivedListener(
888 OnJinglePacketReceived listener) {
889 this.jingleListener = listener;
890 }
891
892 public void setOnStatusChangedListener(OnStatusChanged listener) {
893 this.statusListener = listener;
894 }
895
896 public void setOnBindListener(OnBindListener listener) {
897 this.bindListener = listener;
898 }
899
900 public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) {
901 this.acknowledgedListener = listener;
902 }
903
904 public void disconnect(boolean force) {
905 changeStatus(Account.STATUS_OFFLINE);
906 Log.d(Config.LOGTAG, "disconnecting");
907 try {
908 if (force) {
909 socket.close();
910 return;
911 }
912 new Thread(new Runnable() {
913
914 @Override
915 public void run() {
916 if (tagWriter.isActive()) {
917 tagWriter.finish();
918 try {
919 while (!tagWriter.finished()) {
920 Log.d(Config.LOGTAG, "not yet finished");
921 Thread.sleep(100);
922 }
923 tagWriter.writeTag(Tag.end("stream:stream"));
924 } catch (IOException e) {
925 Log.d(Config.LOGTAG,
926 "io exception during disconnect");
927 } catch (InterruptedException e) {
928 Log.d(Config.LOGTAG, "interrupted");
929 }
930 }
931 }
932 }).start();
933 } catch (IOException e) {
934 Log.d(Config.LOGTAG, "io exception during disconnect");
935 }
936 }
937
938 public List<String> findDiscoItemsByFeature(String feature) {
939 List<String> items = new ArrayList<String>();
940 for (Entry<String, List<String>> cursor : disco.entrySet()) {
941 if (cursor.getValue().contains(feature)) {
942 items.add(cursor.getKey());
943 }
944 }
945 return items;
946 }
947
948 public String findDiscoItemByFeature(String feature) {
949 List<String> items = findDiscoItemsByFeature(feature);
950 if (items.size() >= 1) {
951 return items.get(0);
952 }
953 return null;
954 }
955
956 public void r() {
957 this.tagWriter.writeStanzaAsync(new RequestPacket(smVersion));
958 }
959
960 public String getMucServer() {
961 return findDiscoItemByFeature("http://jabber.org/protocol/muc");
962 }
963
964 public int getTimeToNextAttempt() {
965 int interval = (int) (25 * Math.pow(1.5, attempt));
966 int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000);
967 return interval - secondsSinceLast;
968 }
969
970 public int getAttempt() {
971 return this.attempt;
972 }
973
974 public Features getFeatures() {
975 return this.features;
976 }
977
978 public class Features {
979 XmppConnection connection;
980
981 public Features(XmppConnection connection) {
982 this.connection = connection;
983 }
984
985 private boolean hasDiscoFeature(String server, String feature) {
986 if (!connection.disco.containsKey(server)) {
987 return false;
988 }
989 return connection.disco.get(server).contains(feature);
990 }
991
992 public boolean carbons() {
993 return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2");
994 }
995
996 public boolean sm() {
997 return streamId != null;
998 }
999
1000 public boolean csi() {
1001 if (connection.streamFeatures == null) {
1002 return false;
1003 } else {
1004 return connection.streamFeatures.hasChild("csi",
1005 "urn:xmpp:csi:0");
1006 }
1007 }
1008
1009 public boolean pubsub() {
1010 return hasDiscoFeature(account.getServer(),
1011 "http://jabber.org/protocol/pubsub#publish");
1012 }
1013
1014 public boolean rosterVersioning() {
1015 if (connection.streamFeatures == null) {
1016 return false;
1017 } else {
1018 return connection.streamFeatures.hasChild("ver");
1019 }
1020 }
1021
1022 public boolean streamhost() {
1023 return connection
1024 .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null;
1025 }
1026
1027 public boolean compression() {
1028 return connection.usingCompression;
1029 }
1030 }
1031
1032 public long getLastSessionEstablished() {
1033 long diff;
1034 if (this.lastSessionStarted == 0) {
1035 diff = SystemClock.elapsedRealtime() - this.lastConnect;
1036 } else {
1037 diff = SystemClock.elapsedRealtime() - this.lastSessionStarted;
1038 }
1039 return System.currentTimeMillis() - diff;
1040 }
1041
1042 public long getLastConnect() {
1043 return this.lastConnect;
1044 }
1045
1046 public long getLastPingSent() {
1047 return this.lastPingSent;
1048 }
1049
1050 public long getLastPacketReceived() {
1051 return this.lastPaketReceived;
1052 }
1053
1054 public void sendActive() {
1055 this.sendPacket(new ActivePacket(), null);
1056 }
1057
1058 public void sendInactive() {
1059 this.sendPacket(new InactivePacket(), null);
1060 }
1061}