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