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