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