1package eu.siacs.conversations.services;
2
3import java.text.ParseException;
4import java.text.SimpleDateFormat;
5import java.util.Date;
6import java.util.Hashtable;
7import java.util.List;
8import java.util.Random;
9
10import org.json.JSONException;
11import org.openintents.openpgp.util.OpenPgpApi;
12import org.openintents.openpgp.util.OpenPgpServiceConnection;
13
14import net.java.otr4j.OtrException;
15import net.java.otr4j.session.Session;
16import net.java.otr4j.session.SessionStatus;
17
18import eu.siacs.conversations.crypto.PgpEngine;
19import eu.siacs.conversations.crypto.PgpEngine.OpenPgpException;
20import eu.siacs.conversations.entities.Account;
21import eu.siacs.conversations.entities.Contact;
22import eu.siacs.conversations.entities.Conversation;
23import eu.siacs.conversations.entities.Message;
24import eu.siacs.conversations.entities.MucOptions;
25import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
26import eu.siacs.conversations.entities.Presences;
27import eu.siacs.conversations.persistance.DatabaseBackend;
28import eu.siacs.conversations.persistance.OnPhoneContactsMerged;
29import eu.siacs.conversations.ui.OnAccountListChangedListener;
30import eu.siacs.conversations.ui.OnConversationListChangedListener;
31import eu.siacs.conversations.ui.OnRosterFetchedListener;
32import eu.siacs.conversations.utils.MessageParser;
33import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
34import eu.siacs.conversations.utils.PhoneHelper;
35import eu.siacs.conversations.utils.UIHelper;
36import eu.siacs.conversations.xml.Element;
37import eu.siacs.conversations.xmpp.IqPacket;
38import eu.siacs.conversations.xmpp.MessagePacket;
39import eu.siacs.conversations.xmpp.OnIqPacketReceived;
40import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
41import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
42import eu.siacs.conversations.xmpp.OnStatusChanged;
43import eu.siacs.conversations.xmpp.OnTLSExceptionReceived;
44import eu.siacs.conversations.xmpp.PresencePacket;
45import eu.siacs.conversations.xmpp.XmppConnection;
46import android.app.AlarmManager;
47import android.app.PendingIntent;
48import android.app.Service;
49import android.content.Context;
50import android.content.Intent;
51import android.content.SharedPreferences;
52import android.database.ContentObserver;
53import android.database.DatabaseUtils;
54import android.net.ConnectivityManager;
55import android.net.NetworkInfo;
56import android.os.Binder;
57import android.os.Bundle;
58import android.os.IBinder;
59import android.os.PowerManager;
60import android.os.SystemClock;
61import android.preference.PreferenceManager;
62import android.provider.ContactsContract;
63import android.util.Log;
64
65public class XmppConnectionService extends Service {
66
67 protected static final String LOGTAG = "xmppService";
68 public DatabaseBackend databaseBackend;
69
70 public long startDate;
71
72 private static final int PING_INTERVAL = 300;
73 private static final int PING_TIMEOUT = 2;
74
75 private List<Account> accounts;
76 private List<Conversation> conversations = null;
77
78 public OnConversationListChangedListener convChangedListener = null;
79 private OnAccountListChangedListener accountChangedListener = null;
80 private OnTLSExceptionReceived tlsException = null;
81
82 public void setOnTLSExceptionReceivedListener(OnTLSExceptionReceived listener) {
83 tlsException = listener;
84 }
85
86 private Random mRandom = new Random(System.currentTimeMillis());
87
88 private ContentObserver contactObserver = new ContentObserver(null) {
89 @Override
90 public void onChange(boolean selfChange) {
91 super.onChange(selfChange);
92 Log.d(LOGTAG, "contact list has changed");
93 mergePhoneContactsWithRoster(null);
94 }
95 };
96
97 private XmppConnectionService service = this;
98
99 private final IBinder mBinder = new XmppConnectionBinder();
100 private OnMessagePacketReceived messageListener = new OnMessagePacketReceived() {
101
102 @Override
103 public void onMessagePacketReceived(Account account,
104 MessagePacket packet) {
105 Message message = null;
106 boolean notify = true;
107 if ((packet.getType() == MessagePacket.TYPE_CHAT)) {
108 String pgpBody = MessageParser.getPgpBody(packet);
109 if (pgpBody != null) {
110 message = MessageParser.parsePgpChat(pgpBody, packet,
111 account, service);
112 message.markUnread();
113 } else if (packet.hasChild("body")
114 && (packet.getBody().startsWith("?OTR"))) {
115 message = MessageParser.parseOtrChat(packet, account,
116 service);
117 if (message!=null) {
118 message.markUnread();
119 }
120 } else if (packet.hasChild("body")) {
121 message = MessageParser.parsePlainTextChat(packet, account,
122 service);
123 message.markUnread();
124 } else if (packet.hasChild("received")
125 || (packet.hasChild("sent"))) {
126 message = MessageParser.parseCarbonMessage(packet, account,
127 service);
128 message.getConversation().markRead();
129 notify = false;
130 }
131
132 } else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) {
133 message = MessageParser
134 .parseGroupchat(packet, account, service);
135 if (message != null) {
136 if (message.getStatus() == Message.STATUS_RECIEVED) {
137 message.markUnread();
138 } else {
139 message.getConversation().markRead();
140 notify = false;
141 }
142 }
143 } else if (packet.getType() == MessagePacket.TYPE_ERROR) {
144 message = MessageParser.parseError(packet, account, service);
145 } else {
146 //Log.d(LOGTAG, "unparsed message " + packet.toString());
147 }
148 if (message == null) {
149 return;
150 }
151 if (packet.hasChild("delay")) {
152 try {
153 String stamp = packet.findChild("delay").getAttribute(
154 "stamp");
155 stamp = stamp.replace("Z", "+0000");
156 Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
157 .parse(stamp);
158 message.setTime(date.getTime());
159 } catch (ParseException e) {
160 Log.d(LOGTAG, "error trying to parse date" + e.getMessage());
161 }
162 }
163 Conversation conversation = message.getConversation();
164 conversation.getMessages().add(message);
165 if (packet.getType() != MessagePacket.TYPE_ERROR) {
166 databaseBackend.createMessage(message);
167 }
168 if (convChangedListener != null) {
169 convChangedListener.onConversationListChanged();
170 } else {
171 UIHelper.updateNotification(getApplicationContext(), getConversations(), notify);
172 }
173 }
174 };
175 private OnStatusChanged statusListener = new OnStatusChanged() {
176
177 @Override
178 public void onStatusChanged(Account account) {
179 if (accountChangedListener != null) {
180 accountChangedListener.onAccountListChangedListener();
181 }
182 if (account.getStatus() == Account.STATUS_ONLINE) {
183 databaseBackend.clearPresences(account);
184 if (account.getXmppConnection().hasFeatureRosterManagment()) {
185 updateRoster(account, null);
186 }
187 connectMultiModeConversations(account);
188 List<Conversation> conversations = getConversations();
189 for (int i = 0; i < conversations.size(); ++i) {
190 if (conversations.get(i).getAccount() == account) {
191 sendUnsendMessages(conversations.get(i));
192 }
193 }
194 if (convChangedListener != null) {
195 convChangedListener.onConversationListChanged();
196 }
197 if (account.getKeys().has("pgp_signature")) {
198 try {
199 sendPgpPresence(account, account.getKeys().getString("pgp_signature"));
200 } catch (JSONException e) {
201 //
202 }
203 }
204 scheduleWakeupCall(PING_INTERVAL, true);
205 } else if (account.getStatus() == Account.STATUS_OFFLINE) {
206 Log.d(LOGTAG,"onStatusChanged offline");
207 databaseBackend.clearPresences(account);
208 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
209 int timeToReconnect = mRandom.nextInt(50)+10;
210 scheduleWakeupCall(timeToReconnect,false);
211 }
212
213 }
214 }
215 };
216
217 private OnPresencePacketReceived presenceListener = new OnPresencePacketReceived() {
218
219 @Override
220 public void onPresencePacketReceived(Account account,
221 PresencePacket packet) {
222 if (packet.hasChild("x")&&(packet.findChild("x").getAttribute("xmlns").startsWith("http://jabber.org/protocol/muc"))) {
223 Conversation muc = findMuc(packet.getAttribute("from").split("/")[0]);
224 if (muc!=null) {
225 int error = muc.getMucOptions().getError();
226 muc.getMucOptions().processPacket(packet);
227 if ((muc.getMucOptions().getError()!=error)&&(convChangedListener!=null)) {
228 Log.d(LOGTAG,"muc error status changed");
229 convChangedListener.onConversationListChanged();
230 }
231 }
232 } else {
233 String[] fromParts = packet.getAttribute("from").split("/");
234 Contact contact = findContact(account, fromParts[0]);
235 if (contact == null) {
236 // most likely self or roster not synced
237 return;
238 }
239 String type = packet.getAttribute("type");
240 if (type == null) {
241 Element show = packet.findChild("show");
242 if (show == null) {
243 contact.updatePresence(fromParts[1], Presences.ONLINE);
244 } else if (show.getContent().equals("away")) {
245 contact.updatePresence(fromParts[1], Presences.AWAY);
246 } else if (show.getContent().equals("xa")) {
247 contact.updatePresence(fromParts[1], Presences.XA);
248 } else if (show.getContent().equals("chat")) {
249 contact.updatePresence(fromParts[1], Presences.CHAT);
250 } else if (show.getContent().equals("dnd")) {
251 contact.updatePresence(fromParts[1], Presences.DND);
252 }
253 PgpEngine pgp = getPgpEngine();
254 if (pgp!=null) {
255 Element x = packet.findChild("x");
256 if ((x != null)
257 && (x.getAttribute("xmlns").equals("jabber:x:signed"))) {
258 try {
259 contact.setPgpKeyId(pgp.fetchKeyId(packet.findChild("status")
260 .getContent(), x.getContent()));
261 } catch (OpenPgpException e) {
262 Log.d(LOGTAG,"faulty pgp. just ignore");
263 }
264 }
265 }
266 databaseBackend.updateContact(contact);
267 } else if (type.equals("unavailable")) {
268 if (fromParts.length != 2) {
269 // Log.d(LOGTAG,"received presence with no resource "+packet.toString());
270 } else {
271 contact.removePresence(fromParts[1]);
272 databaseBackend.updateContact(contact);
273 }
274 } else if (type.equals("subscribe")) {
275 if (contact
276 .getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) {
277 sendPresenceUpdatesTo(contact);
278 contact.setSubscriptionOption(Contact.Subscription.FROM);
279 contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT);
280 replaceContactInConversation(contact.getJid(), contact);
281 databaseBackend.updateContact(contact);
282 if ((contact
283 .getSubscriptionOption(Contact.Subscription.ASKING))
284 && (!contact
285 .getSubscriptionOption(Contact.Subscription.TO))) {
286 requestPresenceUpdatesFrom(contact);
287 }
288 } else {
289 // TODO: ask user to handle it maybe
290 }
291 } else {
292 Log.d(LOGTAG, packet.toString());
293 }
294 replaceContactInConversation(contact.getJid(), contact);
295 }
296 }
297 };
298
299 private OnIqPacketReceived unknownIqListener = new OnIqPacketReceived() {
300
301 @Override
302 public void onIqPacketReceived(Account account, IqPacket packet) {
303 if (packet.hasChild("query")) {
304 Element query = packet.findChild("query");
305 String xmlns = query.getAttribute("xmlns");
306 if ((xmlns != null) && (xmlns.equals("jabber:iq:roster"))) {
307 processRosterItems(account, query);
308 mergePhoneContactsWithRoster(null);
309 }
310 }
311 }
312 };
313
314 private OpenPgpServiceConnection pgpServiceConnection;
315 private PgpEngine mPgpEngine = null;
316
317 public PgpEngine getPgpEngine() {
318 if (pgpServiceConnection.isBound()) {
319 if (this.mPgpEngine == null) {
320 this.mPgpEngine = new PgpEngine(new OpenPgpApi(
321 getApplicationContext(),
322 pgpServiceConnection.getService()));
323 }
324 return mPgpEngine;
325 } else {
326 return null;
327 }
328
329 }
330
331 protected Conversation findMuc(String name) {
332 for(Conversation conversation : this.conversations) {
333 if (conversation.getContactJid().split("/")[0].equals(name)) {
334 return conversation;
335 }
336 }
337 return null;
338 }
339
340 private void processRosterItems(Account account, Element elements) {
341 String version = elements.getAttribute("ver");
342 if (version != null) {
343 account.setRosterVersion(version);
344 databaseBackend.updateAccount(account);
345 }
346 for (Element item : elements.getChildren()) {
347 if (item.getName().equals("item")) {
348 String jid = item.getAttribute("jid");
349 String subscription = item.getAttribute("subscription");
350 Contact contact = databaseBackend.findContact(account, jid);
351 if (contact == null) {
352 if (!subscription.equals("remove")) {
353 String name = item.getAttribute("name");
354 if (name == null) {
355 name = jid.split("@")[0];
356 }
357 contact = new Contact(account, name, jid, null);
358 contact.parseSubscriptionFromElement(item);
359 databaseBackend.createContact(contact);
360 }
361 } else {
362 if (subscription.equals("remove")) {
363 databaseBackend.deleteContact(contact);
364 replaceContactInConversation(contact.getJid(), null);
365 } else {
366 contact.parseSubscriptionFromElement(item);
367 databaseBackend.updateContact(contact);
368 replaceContactInConversation(contact.getJid(), contact);
369 }
370 }
371 }
372 }
373 }
374
375 private void replaceContactInConversation(String jid, Contact contact) {
376 List<Conversation> conversations = getConversations();
377 for (int i = 0; i < conversations.size(); ++i) {
378 if ((conversations.get(i).getContactJid().equals(jid))) {
379 conversations.get(i).setContact(contact);
380 break;
381 }
382 }
383 }
384
385 public class XmppConnectionBinder extends Binder {
386 public XmppConnectionService getService() {
387 return XmppConnectionService.this;
388 }
389 }
390
391 @Override
392 public int onStartCommand(Intent intent, int flags, int startId) {
393 ConnectivityManager cm = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
394
395 NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
396 boolean isConnected = activeNetwork != null
397 && activeNetwork.isConnected();
398 for (Account account : accounts) {
399 if (!isConnected) {
400 account.setStatus(Account.STATUS_NO_INTERNET);
401 } else {
402 if (account.getStatus() == Account.STATUS_NO_INTERNET) {
403 account.setStatus(Account.STATUS_OFFLINE);
404 }
405 }
406 if (accountChangedListener!=null) {
407 accountChangedListener.onAccountListChangedListener();
408 }
409 if ((!account.isOptionSet(Account.OPTION_DISABLED))&&(isConnected)) {
410 if (account.getXmppConnection() == null) {
411 account.setXmppConnection(this.createConnection(account));
412 }
413 if (account.getStatus()==Account.STATUS_OFFLINE) {
414 Thread thread = new Thread(account.getXmppConnection());
415 thread.start();
416 } else {
417 if (intent.getBooleanExtra("ping", false)) {
418 Log.d(LOGTAG,"start service ping");
419 ping(account,PING_TIMEOUT);
420 }
421 }
422 }
423 }
424 return START_STICKY;
425 }
426
427 @Override
428 public void onCreate() {
429 databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
430 this.accounts = databaseBackend.getAccounts();
431
432 getContentResolver().registerContentObserver(
433 ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
434 this.pgpServiceConnection = new OpenPgpServiceConnection(
435 getApplicationContext(), "org.sufficientlysecure.keychain");
436 this.pgpServiceConnection.bindToService();
437
438
439 }
440
441 @Override
442 public void onDestroy() {
443 super.onDestroy();
444 for (Account account : accounts) {
445 if (account.getXmppConnection() != null) {
446 disconnect(account);
447 }
448 }
449 }
450
451 protected void scheduleWakeupCall(int seconds,boolean ping) {
452 Context context = getApplicationContext();
453 AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
454 Intent intent = new Intent(context, EventReceiver.class);
455 intent.putExtra("ping", ping);
456 PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
457 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
458 SystemClock.elapsedRealtime() +
459 seconds * 1000, alarmIntent);
460 if (ping) {
461 Log.d(LOGTAG,"schedule ping in "+seconds+" seconds");
462 } else {
463 Log.d(LOGTAG,"schedule reconnect in "+seconds+" seconds");
464 }
465
466 }
467
468 public XmppConnection createConnection(Account account) {
469 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
470 XmppConnection connection = new XmppConnection(account, pm);
471 connection.setOnMessagePacketReceivedListener(this.messageListener);
472 connection.setOnStatusChangedListener(this.statusListener);
473 connection.setOnPresencePacketReceivedListener(this.presenceListener);
474 connection
475 .setOnUnregisteredIqPacketReceivedListener(this.unknownIqListener);
476 connection.setOnTLSExceptionReceivedListener(new OnTLSExceptionReceived() {
477
478 @Override
479 public void onTLSExceptionReceived(String fingerprint, Account account) {
480 Log.d(LOGTAG,"tls exception arrived in service");
481 if (tlsException!=null) {
482 tlsException.onTLSExceptionReceived(fingerprint,account);
483 }
484 }
485 });
486 return connection;
487 }
488
489 public void sendMessage(Message message, String presence) {
490 Account account = message.getConversation().getAccount();
491 Conversation conv = message.getConversation();
492 boolean saveInDb = false;
493 boolean addToConversation = false;
494 if (account.getStatus() == Account.STATUS_ONLINE) {
495 MessagePacket packet;
496 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
497 if (!conv.hasValidOtrSession()) {
498 // starting otr session. messages will be send later
499 conv.startOtrSession(getApplicationContext(), presence);
500 } else if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
501 // otr session aleary exists, creating message packet
502 // accordingly
503 packet = prepareMessagePacket(account, message,
504 conv.getOtrSession());
505 account.getXmppConnection().sendMessagePacket(packet);
506 message.setStatus(Message.STATUS_SEND);
507 }
508 saveInDb = true;
509 addToConversation = true;
510 } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
511 long keyId = message.getConversation().getContact()
512 .getPgpKeyId();
513 packet = new MessagePacket();
514 packet.setType(MessagePacket.TYPE_CHAT);
515 packet.setFrom(message.getConversation().getAccount()
516 .getFullJid());
517 packet.setTo(message.getCounterpart());
518 packet.setBody("This is an XEP-0027 encryted message");
519 Element x = new Element("x");
520 x.setAttribute("xmlns", "jabber:x:encrypted");
521 x.setContent(this.getPgpEngine().encrypt(keyId,
522 message.getBody()));
523 packet.addChild(x);
524 account.getXmppConnection().sendMessagePacket(packet);
525 message.setStatus(Message.STATUS_SEND);
526 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
527 saveInDb = true;
528 addToConversation = true;
529 } else {
530 // don't encrypt
531 if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
532 message.setStatus(Message.STATUS_SEND);
533 saveInDb = true;
534 addToConversation = true;
535 }
536
537 packet = prepareMessagePacket(account, message, null);
538 account.getXmppConnection().sendMessagePacket(packet);
539 }
540 } else {
541 // account is offline
542 saveInDb = true;
543 addToConversation = true;
544
545 }
546 if (saveInDb) {
547 databaseBackend.createMessage(message);
548 }
549 if (addToConversation) {
550 conv.getMessages().add(message);
551 if (convChangedListener != null) {
552 convChangedListener.onConversationListChanged();
553 }
554 }
555
556 }
557
558 private void sendUnsendMessages(Conversation conversation) {
559 for (int i = 0; i < conversation.getMessages().size(); ++i) {
560 if (conversation.getMessages().get(i).getStatus() == Message.STATUS_UNSEND) {
561 Message message = conversation.getMessages().get(i);
562 MessagePacket packet = prepareMessagePacket(
563 conversation.getAccount(), message, null);
564 conversation.getAccount().getXmppConnection()
565 .sendMessagePacket(packet);
566 message.setStatus(Message.STATUS_SEND);
567 if (conversation.getMode() == Conversation.MODE_SINGLE) {
568 databaseBackend.updateMessage(message);
569 } else {
570 databaseBackend.deleteMessage(message);
571 conversation.getMessages().remove(i);
572 i--;
573 }
574 }
575 }
576 }
577
578 public MessagePacket prepareMessagePacket(Account account, Message message,
579 Session otrSession) {
580 MessagePacket packet = new MessagePacket();
581 if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
582 packet.setType(MessagePacket.TYPE_CHAT);
583 if (otrSession != null) {
584 try {
585 packet.setBody(otrSession.transformSending(message
586 .getBody()));
587 } catch (OtrException e) {
588 Log.d(LOGTAG,
589 account.getJid()
590 + ": could not encrypt message to "
591 + message.getCounterpart());
592 }
593 Element privateMarker = new Element("private");
594 privateMarker.setAttribute("xmlns", "urn:xmpp:carbons:2");
595 packet.addChild(privateMarker);
596 packet.setTo(otrSession.getSessionID().getAccountID() + "/"
597 + otrSession.getSessionID().getUserID());
598 packet.setFrom(account.getFullJid());
599 } else {
600 packet.setBody(message.getBody());
601 packet.setTo(message.getCounterpart());
602 packet.setFrom(account.getJid());
603 }
604 } else if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
605 packet.setType(MessagePacket.TYPE_GROUPCHAT);
606 packet.setBody(message.getBody());
607 packet.setTo(message.getCounterpart().split("/")[0]);
608 packet.setFrom(account.getJid());
609 }
610 return packet;
611 }
612
613 public void getRoster(Account account,
614 final OnRosterFetchedListener listener) {
615 List<Contact> contacts = databaseBackend.getContactsByAccount(account);
616 for (int i = 0; i < contacts.size(); ++i) {
617 contacts.get(i).setAccount(account);
618 }
619 if (listener != null) {
620 listener.onRosterFetched(contacts);
621 }
622 }
623
624 public void updateRoster(final Account account,
625 final OnRosterFetchedListener listener) {
626 IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
627 Element query = new Element("query");
628 query.setAttribute("xmlns", "jabber:iq:roster");
629 if (!"".equals(account.getRosterVersion())) {
630 Log.d(LOGTAG,account.getJid()+ ": fetching roster version "+account.getRosterVersion());
631 } else {
632 Log.d(LOGTAG,account.getJid()+": fetching roster");
633 }
634 query.setAttribute("ver", account.getRosterVersion());
635 iqPacket.addChild(query);
636 account.getXmppConnection().sendIqPacket(iqPacket,
637 new OnIqPacketReceived() {
638
639 @Override
640 public void onIqPacketReceived(final Account account,
641 IqPacket packet) {
642 Element roster = packet.findChild("query");
643 if (roster != null) {
644 Log.d(LOGTAG,account.getJid()+": processing roster");
645 processRosterItems(account, roster);
646 StringBuilder mWhere = new StringBuilder();
647 mWhere.append("jid NOT IN(");
648 List<Element> items = roster.getChildren();
649 for (int i = 0; i < items.size(); ++i) {
650 mWhere.append(DatabaseUtils
651 .sqlEscapeString(items.get(i)
652 .getAttribute("jid")));
653 if (i != items.size() - 1) {
654 mWhere.append(",");
655 }
656 }
657 mWhere.append(") and accountUuid = \"");
658 mWhere.append(account.getUuid());
659 mWhere.append("\"");
660 List<Contact> contactsToDelete = databaseBackend
661 .getContacts(mWhere.toString());
662 for (Contact contact : contactsToDelete) {
663 databaseBackend.deleteContact(contact);
664 replaceContactInConversation(contact.getJid(),
665 null);
666 }
667
668 } else {
669 Log.d(LOGTAG,account.getJid()+": empty roster returend");
670 }
671 mergePhoneContactsWithRoster(new OnPhoneContactsMerged() {
672
673 @Override
674 public void phoneContactsMerged() {
675 if (listener != null) {
676 getRoster(account, listener);
677 }
678 }
679 });
680 }
681 });
682 }
683
684 public void mergePhoneContactsWithRoster(
685 final OnPhoneContactsMerged listener) {
686 PhoneHelper.loadPhoneContacts(getApplicationContext(),
687 new OnPhoneContactsLoadedListener() {
688 @Override
689 public void onPhoneContactsLoaded(
690 Hashtable<String, Bundle> phoneContacts) {
691 List<Contact> contacts = databaseBackend
692 .getContactsByAccount(null);
693 for (int i = 0; i < contacts.size(); ++i) {
694 Contact contact = contacts.get(i);
695 if (phoneContacts.containsKey(contact.getJid())) {
696 Bundle phoneContact = phoneContacts.get(contact
697 .getJid());
698 String systemAccount = phoneContact
699 .getInt("phoneid")
700 + "#"
701 + phoneContact.getString("lookup");
702 contact.setSystemAccount(systemAccount);
703 contact.setPhotoUri(phoneContact
704 .getString("photouri"));
705 contact.setDisplayName(phoneContact
706 .getString("displayname"));
707 databaseBackend.updateContact(contact);
708 replaceContactInConversation(contact.getJid(),
709 contact);
710 } else {
711 if ((contact.getSystemAccount() != null)
712 || (contact.getProfilePhoto() != null)) {
713 contact.setSystemAccount(null);
714 contact.setPhotoUri(null);
715 databaseBackend.updateContact(contact);
716 replaceContactInConversation(
717 contact.getJid(), contact);
718 }
719 }
720 }
721 if (listener != null) {
722 listener.phoneContactsMerged();
723 }
724 }
725 });
726 }
727
728 public List<Conversation> getConversations() {
729 if (this.conversations == null) {
730 Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
731 for (Account account : this.accounts) {
732 accountLookupTable.put(account.getUuid(), account);
733 }
734 this.conversations = databaseBackend
735 .getConversations(Conversation.STATUS_AVAILABLE);
736 for (Conversation conv : this.conversations) {
737 Account account = accountLookupTable.get(conv.getAccountUuid());
738 conv.setAccount(account);
739 conv.setContact(findContact(account, conv.getContactJid()));
740 conv.setMessages(databaseBackend.getMessages(conv, 50));
741 }
742 }
743 return this.conversations;
744 }
745
746 public List<Account> getAccounts() {
747 return this.accounts;
748 }
749
750 public Contact findContact(Account account, String jid) {
751 Contact contact = databaseBackend.findContact(account, jid);
752 if (contact != null) {
753 contact.setAccount(account);
754 }
755 return contact;
756 }
757
758 public Conversation findOrCreateConversation(Account account, String jid,
759 boolean muc) {
760 for (Conversation conv : this.getConversations()) {
761 if ((conv.getAccount().equals(account))
762 && (conv.getContactJid().split("/")[0].equals(jid))) {
763 return conv;
764 }
765 }
766 Conversation conversation = databaseBackend.findConversation(account,
767 jid);
768 if (conversation != null) {
769 conversation.setStatus(Conversation.STATUS_AVAILABLE);
770 conversation.setAccount(account);
771 if (muc) {
772 conversation.setMode(Conversation.MODE_MULTI);
773 if (account.getStatus() == Account.STATUS_ONLINE) {
774 joinMuc(conversation);
775 }
776 } else {
777 conversation.setMode(Conversation.MODE_SINGLE);
778 }
779 this.databaseBackend.updateConversation(conversation);
780 conversation.setContact(findContact(account,
781 conversation.getContactJid()));
782 } else {
783 String conversationName;
784 Contact contact = findContact(account, jid);
785 if (contact != null) {
786 conversationName = contact.getDisplayName();
787 } else {
788 conversationName = jid.split("@")[0];
789 }
790 if (muc) {
791 conversation = new Conversation(conversationName, account, jid,
792 Conversation.MODE_MULTI);
793 if (account.getStatus() == Account.STATUS_ONLINE) {
794 joinMuc(conversation);
795 }
796 } else {
797 conversation = new Conversation(conversationName, account, jid,
798 Conversation.MODE_SINGLE);
799 }
800 conversation.setContact(contact);
801 this.databaseBackend.createConversation(conversation);
802 }
803 this.conversations.add(conversation);
804 if (this.convChangedListener != null) {
805 this.convChangedListener.onConversationListChanged();
806 }
807 return conversation;
808 }
809
810 public void archiveConversation(Conversation conversation) {
811 if (conversation.getMode() == Conversation.MODE_MULTI) {
812 leaveMuc(conversation);
813 } else {
814 try {
815 conversation.endOtrIfNeeded();
816 } catch (OtrException e) {
817 Log.d(LOGTAG,
818 "error ending otr session for "
819 + conversation.getName());
820 }
821 }
822 this.databaseBackend.updateConversation(conversation);
823 this.conversations.remove(conversation);
824 if (this.convChangedListener != null) {
825 this.convChangedListener.onConversationListChanged();
826 }
827 }
828
829 public int getConversationCount() {
830 return this.databaseBackend.getConversationCount();
831 }
832
833 public void createAccount(Account account) {
834 databaseBackend.createAccount(account);
835 this.accounts.add(account);
836 account.setXmppConnection(this.createConnection(account));
837 if (accountChangedListener != null)
838 accountChangedListener.onAccountListChangedListener();
839 }
840
841 public void deleteContact(Contact contact) {
842 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
843 Element query = new Element("query");
844 query.setAttribute("xmlns", "jabber:iq:roster");
845 Element item = new Element("item");
846 item.setAttribute("jid", contact.getJid());
847 item.setAttribute("subscription", "remove");
848 query.addChild(item);
849 iq.addChild(query);
850 contact.getAccount().getXmppConnection().sendIqPacket(iq, null);
851 replaceContactInConversation(contact.getJid(), null);
852 databaseBackend.deleteContact(contact);
853 }
854
855 public void updateAccount(Account account) {
856 databaseBackend.updateAccount(account);
857 reconnectAccount(account);
858 if (accountChangedListener != null)
859 accountChangedListener.onAccountListChangedListener();
860 }
861
862 public void deleteAccount(Account account) {
863 Log.d(LOGTAG, "called delete account");
864 if (account.getXmppConnection() != null) {
865 this.disconnect(account);
866 }
867 databaseBackend.deleteAccount(account);
868 this.accounts.remove(account);
869 if (accountChangedListener != null)
870 accountChangedListener.onAccountListChangedListener();
871 }
872
873 public void setOnConversationListChangedListener(
874 OnConversationListChangedListener listener) {
875 this.convChangedListener = listener;
876 }
877
878 public void removeOnConversationListChangedListener() {
879 this.convChangedListener = null;
880 }
881
882 public void setOnAccountListChangedListener(
883 OnAccountListChangedListener listener) {
884 this.accountChangedListener = listener;
885 }
886
887 public void removeOnAccountListChangedListener() {
888 this.accountChangedListener = null;
889 }
890
891 public void connectMultiModeConversations(Account account) {
892 List<Conversation> conversations = getConversations();
893 for (int i = 0; i < conversations.size(); i++) {
894 Conversation conversation = conversations.get(i);
895 if ((conversation.getMode() == Conversation.MODE_MULTI)
896 && (conversation.getAccount() == account)) {
897 joinMuc(conversation);
898 }
899 }
900 }
901
902 public void joinMuc(Conversation conversation) {
903 String[] mucParts = conversation.getContactJid().split("/");
904 String muc;
905 String nick;
906 if (mucParts.length == 2) {
907 muc = mucParts[0];
908 nick = mucParts[1];
909 } else {
910 muc = mucParts[0];
911 nick = conversation.getAccount().getUsername();
912 }
913 PresencePacket packet = new PresencePacket();
914 packet.setAttribute("to", muc + "/"
915 + nick);
916 Element x = new Element("x");
917 x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
918 if (conversation.getMessages().size() != 0) {
919 Element history = new Element("history");
920 long lastMsgTime = conversation.getLatestMessage().getTimeSent();
921 long diff = (System.currentTimeMillis() - lastMsgTime) / 1000 - 1;
922 history.setAttribute("seconds", diff + "");
923 x.addChild(history);
924 }
925 packet.addChild(x);
926 conversation.getAccount().getXmppConnection()
927 .sendPresencePacket(packet);
928 }
929
930 private OnRenameListener renameListener = null;
931 private boolean pongReceived;
932 public void setOnRenameListener(OnRenameListener listener) {
933 this.renameListener = listener;
934 }
935
936 public void renameInMuc(final Conversation conversation, final String nick) {
937 final MucOptions options = conversation.getMucOptions();
938 if (options.online()) {
939 options.setOnRenameListener(new OnRenameListener() {
940
941 @Override
942 public void onRename(boolean success) {
943 if (renameListener!=null) {
944 renameListener.onRename(success);
945 }
946 if (success) {
947 databaseBackend.updateConversation(conversation);
948 }
949 }
950 });
951 PresencePacket packet = new PresencePacket();
952 packet.setAttribute("to", conversation.getContactJid().split("/")[0]+"/"+nick);
953 packet.setAttribute("from", conversation.getAccount().getFullJid());
954
955 packet = conversation.getAccount().getXmppConnection().sendPresencePacket(packet, new OnPresencePacketReceived() {
956
957 @Override
958 public void onPresencePacketReceived(Account account, PresencePacket packet) {
959 final boolean changed;
960 String type = packet.getAttribute("type");
961 changed = (!"error".equals(type));
962 if (!changed) {
963 options.getOnRenameListener().onRename(false);
964 } else {
965 if (type==null) {
966 options.getOnRenameListener().onRename(true);
967 options.setNick(packet.getAttribute("from").split("/")[1]);
968 } else {
969 options.processPacket(packet);
970 }
971 }
972 }
973 });
974 } else {
975 String jid = conversation.getContactJid().split("/")[0]+"/"+nick;
976 conversation.setContactJid(jid);
977 databaseBackend.updateConversation(conversation);
978 if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
979 joinMuc(conversation);
980 }
981 }
982 }
983
984 public void leaveMuc(Conversation conversation) {
985 PresencePacket packet = new PresencePacket();
986 packet.setAttribute("to", conversation.getContactJid());
987 packet.setAttribute("from", conversation.getAccount().getFullJid());
988 packet.setAttribute("type","unavailable");
989 conversation.getAccount().getXmppConnection().sendPresencePacket(packet);
990 conversation.getMucOptions().setOffline();
991 }
992
993 public void disconnect(Account account) {
994 List<Conversation> conversations = getConversations();
995 for (int i = 0; i < conversations.size(); i++) {
996 Conversation conversation = conversations.get(i);
997 if (conversation.getAccount() == account) {
998 if (conversation.getMode() == Conversation.MODE_MULTI) {
999 leaveMuc(conversation);
1000 } else {
1001 try {
1002 conversation.endOtrIfNeeded();
1003 } catch (OtrException e) {
1004 Log.d(LOGTAG, "error ending otr session for "
1005 + conversation.getName());
1006 }
1007 }
1008 }
1009 }
1010 account.getXmppConnection().disconnect();
1011 Log.d(LOGTAG, "disconnected account: " + account.getJid());
1012 account.setXmppConnection(null);
1013 }
1014
1015 @Override
1016 public IBinder onBind(Intent intent) {
1017 return mBinder;
1018 }
1019
1020 public void updateContact(Contact contact) {
1021 databaseBackend.updateContact(contact);
1022 replaceContactInConversation(contact.getJid(), contact);
1023 }
1024
1025 public void updateMessage(Message message) {
1026 databaseBackend.updateMessage(message);
1027 }
1028
1029 public void createContact(Contact contact) {
1030 SharedPreferences sharedPref = PreferenceManager
1031 .getDefaultSharedPreferences(getApplicationContext());
1032 boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1033 if (autoGrant) {
1034 contact.setSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT);
1035 contact.setSubscriptionOption(Contact.Subscription.ASKING);
1036 }
1037 databaseBackend.createContact(contact);
1038 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1039 Element query = new Element("query");
1040 query.setAttribute("xmlns", "jabber:iq:roster");
1041 Element item = new Element("item");
1042 item.setAttribute("jid", contact.getJid());
1043 item.setAttribute("name", contact.getJid());
1044 query.addChild(item);
1045 iq.addChild(query);
1046 Account account = contact.getAccount();
1047 account.getXmppConnection().sendIqPacket(iq, null);
1048 if (autoGrant) {
1049 requestPresenceUpdatesFrom(contact);
1050 }
1051 replaceContactInConversation(contact.getJid(), contact);
1052 }
1053
1054 public void requestPresenceUpdatesFrom(Contact contact) {
1055 // Requesting a Subscription type=subscribe
1056 PresencePacket packet = new PresencePacket();
1057 packet.setAttribute("type", "subscribe");
1058 packet.setAttribute("to", contact.getJid());
1059 packet.setAttribute("from", contact.getAccount().getJid());
1060 Log.d(LOGTAG, packet.toString());
1061 contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1062 }
1063
1064 public void stopPresenceUpdatesFrom(Contact contact) {
1065 // Unsubscribing type='unsubscribe'
1066 PresencePacket packet = new PresencePacket();
1067 packet.setAttribute("type", "unsubscribe");
1068 packet.setAttribute("to", contact.getJid());
1069 packet.setAttribute("from", contact.getAccount().getJid());
1070 Log.d(LOGTAG, packet.toString());
1071 contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1072 }
1073
1074 public void stopPresenceUpdatesTo(Contact contact) {
1075 // Canceling a Subscription type=unsubscribed
1076 PresencePacket packet = new PresencePacket();
1077 packet.setAttribute("type", "unsubscribed");
1078 packet.setAttribute("to", contact.getJid());
1079 packet.setAttribute("from", contact.getAccount().getJid());
1080 Log.d(LOGTAG, packet.toString());
1081 contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1082 }
1083
1084 public void sendPresenceUpdatesTo(Contact contact) {
1085 // type='subscribed'
1086 PresencePacket packet = new PresencePacket();
1087 packet.setAttribute("type", "subscribed");
1088 packet.setAttribute("to", contact.getJid());
1089 packet.setAttribute("from", contact.getAccount().getJid());
1090 Log.d(LOGTAG, packet.toString());
1091 contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1092 }
1093
1094 public void sendPgpPresence(Account account, String signature) {
1095 PresencePacket packet = new PresencePacket();
1096 packet.setAttribute("from", account.getFullJid());
1097 Element status = new Element("status");
1098 status.setContent("online");
1099 packet.addChild(status);
1100 Element x = new Element("x");
1101 x.setAttribute("xmlns", "jabber:x:signed");
1102 x.setContent(signature);
1103 packet.addChild(x);
1104 account.getXmppConnection().sendPresencePacket(packet);
1105 }
1106
1107 public void generatePgpAnnouncement(Account account)
1108 throws PgpEngine.UserInputRequiredException {
1109 if (account.getStatus() == Account.STATUS_ONLINE) {
1110 String signature = getPgpEngine().generateSignature("online");
1111 account.setKey("pgp_signature", signature);
1112 databaseBackend.updateAccount(account);
1113 sendPgpPresence(account, signature);
1114 }
1115 }
1116
1117 public void updateConversation(Conversation conversation) {
1118 this.databaseBackend.updateConversation(conversation);
1119 }
1120
1121 public Contact findContact(String uuid) {
1122 Contact contact = this.databaseBackend.getContact(uuid);
1123 for(Account account : getAccounts()) {
1124 if (contact.getAccountUuid().equals(account.getUuid())) {
1125 contact.setAccount(account);
1126 }
1127 }
1128 return contact;
1129 }
1130
1131 public void removeOnTLSExceptionReceivedListener() {
1132 this.tlsException = null;
1133 }
1134
1135 public void reconnectAccount(Account account) {
1136 if (account.getXmppConnection() != null) {
1137 disconnect(account);
1138 }
1139 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1140 if (account.getXmppConnection()==null) {
1141 account.setXmppConnection(this.createConnection(account));
1142 }
1143 Thread thread = new Thread(account.getXmppConnection());
1144 thread.start();
1145 }
1146 }
1147
1148 public void ping(final Account account,final int timeout) {
1149 Log.d(LOGTAG,account.getJid()+": sending ping");
1150 IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
1151 Element ping = new Element("ping");
1152 iq.setAttribute("from",account.getFullJid());
1153 ping.setAttribute("xmlns", "urn:xmpp:ping");
1154 iq.addChild(ping);
1155 pongReceived = false;
1156 account.getXmppConnection().sendIqPacket(iq, new OnIqPacketReceived() {
1157
1158 @Override
1159 public void onIqPacketReceived(Account account, IqPacket packet) {
1160 pongReceived = true;
1161 }
1162 });
1163 new Thread(new Runnable() {
1164
1165 @Override
1166 public void run() {
1167 int i = 0;
1168 while(i <= (5 * timeout)) {
1169 if (pongReceived) {
1170 scheduleWakeupCall(PING_INTERVAL,true);
1171 break;
1172 }
1173 try {
1174 Thread.sleep(200);
1175 } catch (InterruptedException e) {
1176
1177 }
1178 ++i;
1179 }
1180 if (!pongReceived) {
1181 Log.d("xmppService",account.getJid()+" no pong after "+timeout+" seconds");
1182 reconnectAccount(account);
1183 }
1184
1185 }
1186 }).start();
1187 }
1188}