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