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