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