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