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;
19import eu.siacs.conversations.crypto.PgpEngine;
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.parser.MessageParser;
28import eu.siacs.conversations.persistance.DatabaseBackend;
29import eu.siacs.conversations.persistance.FileBackend;
30import eu.siacs.conversations.ui.OnAccountListChangedListener;
31import eu.siacs.conversations.ui.OnConversationListChangedListener;
32import eu.siacs.conversations.ui.UiCallback;
33import eu.siacs.conversations.utils.ExceptionHelper;
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.OnBindListener;
39import eu.siacs.conversations.xmpp.OnContactStatusChanged;
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.XmppConnection;
46import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
47import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
48import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
49import eu.siacs.conversations.xmpp.stanzas.IqPacket;
50import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
51import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
52import android.app.AlarmManager;
53import android.app.PendingIntent;
54import android.app.Service;
55import android.content.Context;
56import android.content.Intent;
57import android.content.SharedPreferences;
58import android.database.ContentObserver;
59import android.net.ConnectivityManager;
60import android.net.NetworkInfo;
61import android.net.Uri;
62import android.os.Binder;
63import android.os.Bundle;
64import android.os.IBinder;
65import android.os.PowerManager;
66import android.os.PowerManager.WakeLock;
67import android.os.SystemClock;
68import android.preference.PreferenceManager;
69import android.provider.ContactsContract;
70import android.util.Log;
71
72public class XmppConnectionService extends Service {
73
74 protected static final String LOGTAG = "xmppService";
75 public DatabaseBackend databaseBackend;
76 private FileBackend fileBackend;
77
78 public long startDate;
79
80 private static final int PING_MAX_INTERVAL = 300;
81 private static final int PING_MIN_INTERVAL = 10;
82 private static final int PING_TIMEOUT = 5;
83 private static final int CONNECT_TIMEOUT = 60;
84 private static final long CARBON_GRACE_PERIOD = 60000L;
85
86 private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
87
88 private MessageParser mMessageParser = new MessageParser(this);
89
90 private List<Account> accounts;
91 private List<Conversation> conversations = null;
92 private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
93 this);
94
95 private OnConversationListChangedListener convChangedListener = null;
96 private int convChangedListenerCount = 0;
97 private OnAccountListChangedListener accountChangedListener = null;
98 private OnTLSExceptionReceived tlsException = null;
99 private OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
100
101 @Override
102 public void onContactStatusChanged(Contact contact) {
103 Conversation conversation = findActiveConversation(contact);
104 if (conversation != null) {
105 conversation.endOtrIfNeeded();
106 }
107 }
108 };
109
110 public void setOnTLSExceptionReceivedListener(
111 OnTLSExceptionReceived listener) {
112 tlsException = listener;
113 }
114
115 private Random mRandom = new Random(System.currentTimeMillis());
116
117 private long lastCarbonMessageReceived = -CARBON_GRACE_PERIOD;
118
119 private ContentObserver contactObserver = new ContentObserver(null) {
120 @Override
121 public void onChange(boolean selfChange) {
122 super.onChange(selfChange);
123 Intent intent = new Intent(getApplicationContext(),
124 XmppConnectionService.class);
125 intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
126 startService(intent);
127 }
128 };
129
130 private final IBinder mBinder = new XmppConnectionBinder();
131 private OnMessagePacketReceived messageListener = new OnMessagePacketReceived() {
132
133 @Override
134 public void onMessagePacketReceived(Account account,
135 MessagePacket packet) {
136 Message message = null;
137 boolean notify = true;
138 if (getPreferences().getBoolean(
139 "notification_grace_period_after_carbon_received", true)) {
140 notify = (SystemClock.elapsedRealtime() - lastCarbonMessageReceived) > CARBON_GRACE_PERIOD;
141 }
142
143 if ((packet.getType() == MessagePacket.TYPE_CHAT)) {
144 if ((packet.getBody() != null)
145 && (packet.getBody().startsWith("?OTR"))) {
146 message = mMessageParser.parseOtrChat(packet, account);
147 if (message != null) {
148 message.markUnread();
149 }
150 } else if (packet.hasChild("body")) {
151 message = mMessageParser.parseChat(packet, account);
152 message.markUnread();
153 } else if (packet.hasChild("received")
154 || (packet.hasChild("sent"))) {
155 message = mMessageParser
156 .parseCarbonMessage(packet, account);
157 if (message != null) {
158 if (message.getStatus() == Message.STATUS_SEND) {
159 lastCarbonMessageReceived = SystemClock
160 .elapsedRealtime();
161 notify = false;
162 message.getConversation().markRead();
163 } else {
164 message.markUnread();
165 }
166 }
167 }
168
169 } else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) {
170 message = mMessageParser.parseGroupchat(packet, account);
171 if (message != null) {
172 if (message.getStatus() == Message.STATUS_RECIEVED) {
173 message.markUnread();
174 } else {
175 message.getConversation().markRead();
176 notify = false;
177 }
178 }
179 } else if (packet.getType() == MessagePacket.TYPE_ERROR) {
180 mMessageParser.parseError(packet, account);
181 return;
182 } else if (packet.getType() == MessagePacket.TYPE_NORMAL) {
183 if (packet.hasChild("displayed","urn:xmpp:chat-markers:0")) {
184 String id = packet.findChild("displayed","urn:xmpp:chat-markers:0").getAttribute("id");
185 String[] fromParts = packet.getFrom().split("/");
186 markMessage(account,fromParts[0], id, Message.STATUS_SEND_DISPLAYED);
187 Log.d(LOGTAG,"message was displayed by contact");
188 } else if (packet.hasChild("received","urn:xmpp:chat-markers:0")) {
189 String id = packet.findChild("received","urn:xmpp:chat-markers:0").getAttribute("id");
190 String[] fromParts = packet.getFrom().split("/");
191 markMessage(account,fromParts[0], id, Message.STATUS_SEND_RECEIVED);
192 } else if (packet.hasChild("x")) {
193 Element x = packet.findChild("x");
194 if (x.hasChild("invite")) {
195 findOrCreateConversation(account, packet.getFrom(),
196 true);
197 if (convChangedListener != null) {
198 convChangedListener.onConversationListChanged();
199 }
200 Log.d(LOGTAG,
201 "invitation received to " + packet.getFrom());
202 }
203
204 } else {
205 //Log.d(LOGTAG, "unparsed message " + packet.toString());
206 }
207 }
208 if ((message == null) || (message.getBody() == null)) {
209 return;
210 }
211 if (packet.hasChild("delay")) {
212 try {
213 String stamp = packet.findChild("delay").getAttribute(
214 "stamp");
215 stamp = stamp.replace("Z", "+0000");
216 Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
217 .parse(stamp);
218 message.setTime(date.getTime());
219 } catch (ParseException e) {
220 Log.d(LOGTAG, "error trying to parse date" + e.getMessage());
221 }
222 }
223 if ((confirmMessages()) && ((packet.getId() != null))) {
224 MessagePacket receivedPacket = new MessagePacket();
225 receivedPacket.setType(MessagePacket.TYPE_NORMAL);
226 receivedPacket.setTo(message.getCounterpart());
227 receivedPacket.setFrom(account.getFullJid());
228 if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
229 Element received = receivedPacket.addChild("received",
230 "urn:xmpp:chat-markers:0");
231 received.setAttribute("id", packet.getId());
232 account.getXmppConnection().sendMessagePacket(
233 receivedPacket);
234 } else if (packet.hasChild("request", "urn:xmpp:receipts")) {
235 Element received = receivedPacket.addChild("received",
236 "urn:xmpp:receipts");
237 received.setAttribute("id", packet.getId());
238 account.getXmppConnection().sendMessagePacket(
239 receivedPacket);
240 }
241 }
242 Conversation conversation = message.getConversation();
243 conversation.getMessages().add(message);
244 if (packet.getType() != MessagePacket.TYPE_ERROR) {
245 databaseBackend.createMessage(message);
246 }
247 if (convChangedListener != null) {
248 convChangedListener.onConversationListChanged();
249 } else {
250 UIHelper.updateNotification(getApplicationContext(),
251 getConversations(), message.getConversation(), notify);
252 }
253 }
254 };
255 private OnStatusChanged statusListener = new OnStatusChanged() {
256
257 @Override
258 public void onStatusChanged(Account account) {
259 if (accountChangedListener != null) {
260 accountChangedListener.onAccountListChangedListener();
261 }
262 if (account.getStatus() == Account.STATUS_ONLINE) {
263 List<Conversation> conversations = getConversations();
264 for (int i = 0; i < conversations.size(); ++i) {
265 if (conversations.get(i).getAccount() == account) {
266 sendUnsendMessages(conversations.get(i));
267 }
268 }
269 syncDirtyContacts(account);
270 scheduleWakeupCall(PING_MAX_INTERVAL, true);
271 } else if (account.getStatus() == Account.STATUS_OFFLINE) {
272 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
273 int timeToReconnect = mRandom.nextInt(50) + 10;
274 scheduleWakeupCall(timeToReconnect, false);
275 }
276
277 } else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) {
278 databaseBackend.updateAccount(account);
279 reconnectAccount(account, true);
280 } else if ((account.getStatus() != Account.STATUS_CONNECTING)
281 && (account.getStatus() != Account.STATUS_NO_INTERNET)) {
282 int next = account.getXmppConnection().getTimeToNextAttempt();
283 Log.d(LOGTAG, account.getJid()
284 + ": error connecting account. try again in " + next
285 + "s for the "
286 + (account.getXmppConnection().getAttempt() + 1)
287 + " time");
288 scheduleWakeupCall(next, false);
289 }
290 UIHelper.showErrorNotification(getApplicationContext(),
291 getAccounts());
292 }
293 };
294
295 private OnPresencePacketReceived presenceListener = new OnPresencePacketReceived() {
296
297 @Override
298 public void onPresencePacketReceived(final Account account,
299 PresencePacket packet) {
300 if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) {
301 Conversation muc = findMuc(
302 packet.getAttribute("from").split("/")[0], account);
303 if (muc != null) {
304 muc.getMucOptions().processPacket(packet, getPgpEngine());
305 } else {
306 Log.d(LOGTAG, account.getJid()
307 + ": could not find muc for received muc package "
308 + packet.toString());
309 }
310 } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
311 Conversation muc = findMuc(
312 packet.getAttribute("from").split("/")[0], account);
313 if (muc != null) {
314 Log.d(LOGTAG,
315 account.getJid() + ": reading muc status packet "
316 + packet.toString());
317 int error = muc.getMucOptions().getError();
318 muc.getMucOptions().processPacket(packet, getPgpEngine());
319 if ((muc.getMucOptions().getError() != error)
320 && (convChangedListener != null)) {
321 Log.d(LOGTAG, "muc error status changed");
322 convChangedListener.onConversationListChanged();
323 }
324 }
325 } else {
326 String[] fromParts = packet.getAttribute("from").split("/");
327 String type = packet.getAttribute("type");
328 if (fromParts[0].equals(account.getJid())) {
329 if (fromParts.length == 2) {
330 if (type == null) {
331 account.updatePresence(fromParts[1], Presences
332 .parseShow(packet.findChild("show")));
333 } else if (type.equals("unavailable")) {
334 account.removePresence(fromParts[1]);
335 }
336 }
337
338 } else {
339 Contact contact = account.getRoster().getContact(
340 packet.getFrom());
341 if (type == null) {
342 if (fromParts.length == 2) {
343 contact.updatePresence(fromParts[1], Presences
344 .parseShow(packet.findChild("show")));
345 PgpEngine pgp = getPgpEngine();
346 if (pgp != null) {
347 Element x = packet.findChild("x",
348 "jabber:x:signed");
349 if (x != null) {
350 Element status = packet.findChild("status");
351 String msg;
352 if (status != null) {
353 msg = status.getContent();
354 } else {
355 msg = "";
356 }
357 contact.setPgpKeyId(pgp.fetchKeyId(account,
358 msg, x.getContent()));
359 }
360 }
361 onContactStatusChanged
362 .onContactStatusChanged(contact);
363 }
364 } else if (type.equals("unavailable")) {
365 if (fromParts.length != 2) {
366 contact.clearPresences();
367 } else {
368 contact.removePresence(fromParts[1]);
369 }
370 onContactStatusChanged.onContactStatusChanged(contact);
371 } else if (type.equals("subscribe")) {
372 Log.d(LOGTAG, "received subscribe packet from "
373 + packet.getFrom());
374 if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
375 Log.d(LOGTAG, "preemptive grant; granting");
376 sendPresenceUpdatesTo(contact);
377 contact.setOption(Contact.Options.FROM);
378 contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
379 if ((contact.getOption(Contact.Options.ASKING))
380 && (!contact.getOption(Contact.Options.TO))) {
381 requestPresenceUpdatesFrom(contact);
382 }
383 } else {
384 contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
385 }
386 } else {
387 // Log.d(LOGTAG, packet.toString());
388 }
389 }
390 }
391 }
392 };
393
394 private OnIqPacketReceived unknownIqListener = new OnIqPacketReceived() {
395
396 @Override
397 public void onIqPacketReceived(Account account, IqPacket packet) {
398 if (packet.hasChild("query", "jabber:iq:roster")) {
399 String from = packet.getFrom();
400 if ((from == null) || (from.equals(account.getJid()))) {
401 Element query = packet.findChild("query");
402 processRosterItems(account, query);
403 } else {
404 Log.d(LOGTAG, "unauthorized roster push from: " + from);
405 }
406 } else if (packet
407 .hasChild("open", "http://jabber.org/protocol/ibb")
408 || packet
409 .hasChild("data", "http://jabber.org/protocol/ibb")) {
410 XmppConnectionService.this.mJingleConnectionManager
411 .deliverIbbPacket(account, packet);
412 } else if (packet.hasChild("query",
413 "http://jabber.org/protocol/disco#info")) {
414 IqPacket iqResponse = packet
415 .generateRespone(IqPacket.TYPE_RESULT);
416 Element query = iqResponse.addChild("query",
417 "http://jabber.org/protocol/disco#info");
418 query.addChild("feature").setAttribute("var",
419 "urn:xmpp:jingle:1");
420 query.addChild("feature").setAttribute("var",
421 "urn:xmpp:jingle:apps:file-transfer:3");
422 query.addChild("feature").setAttribute("var",
423 "urn:xmpp:jingle:transports:s5b:1");
424 query.addChild("feature").setAttribute("var",
425 "urn:xmpp:jingle:transports:ibb:1");
426 if (confirmMessages()) {
427 query.addChild("feature").setAttribute("var",
428 "urn:xmpp:receipts");
429 }
430 account.getXmppConnection().sendIqPacket(iqResponse, null);
431 } else {
432 if ((packet.getType() == IqPacket.TYPE_GET)
433 || (packet.getType() == IqPacket.TYPE_SET)) {
434 IqPacket response = packet
435 .generateRespone(IqPacket.TYPE_ERROR);
436 Element error = response.addChild("error");
437 error.setAttribute("type", "cancel");
438 error.addChild("feature-not-implemented",
439 "urn:ietf:params:xml:ns:xmpp-stanzas");
440 account.getXmppConnection().sendIqPacket(response, null);
441 }
442 }
443 }
444 };
445
446 private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
447
448 @Override
449 public void onJinglePacketReceived(Account account, JinglePacket packet) {
450 mJingleConnectionManager.deliverPacket(account, packet);
451 }
452 };
453
454 private OpenPgpServiceConnection pgpServiceConnection;
455 private PgpEngine mPgpEngine = null;
456 private Intent pingIntent;
457 private PendingIntent pendingPingIntent = null;
458 private WakeLock wakeLock;
459 private PowerManager pm;
460
461 public PgpEngine getPgpEngine() {
462 if (pgpServiceConnection.isBound()) {
463 if (this.mPgpEngine == null) {
464 this.mPgpEngine = new PgpEngine(new OpenPgpApi(
465 getApplicationContext(),
466 pgpServiceConnection.getService()), this);
467 }
468 return mPgpEngine;
469 } else {
470 return null;
471 }
472
473 }
474
475 public FileBackend getFileBackend() {
476 return this.fileBackend;
477 }
478
479 public Message attachImageToConversation(final Conversation conversation,
480 final Uri uri, final UiCallback callback) {
481 final Message message;
482 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
483 message = new Message(conversation, "",
484 Message.ENCRYPTION_DECRYPTED);
485 } else {
486 message = new Message(conversation, "", Message.ENCRYPTION_NONE);
487 }
488 message.setPresence(conversation.getNextPresence());
489 message.setType(Message.TYPE_IMAGE);
490 message.setStatus(Message.STATUS_OFFERED);
491 new Thread(new Runnable() {
492
493 @Override
494 public void run() {
495 try {
496 getFileBackend().copyImageToPrivateStorage(message, uri);
497 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
498 getPgpEngine().encrypt(message, callback);
499 } else {
500 callback.success();
501 }
502 } catch (FileBackend.ImageCopyException e) {
503 callback.error(e.getResId());
504 }
505 }
506 }).start();
507 return message;
508 }
509
510 protected Conversation findMuc(String name, Account account) {
511 for (Conversation conversation : this.conversations) {
512 if (conversation.getContactJid().split("/")[0].equals(name)
513 && (conversation.getAccount() == account)) {
514 return conversation;
515 }
516 }
517 return null;
518 }
519
520 private void processRosterItems(Account account, Element elements) {
521 String version = elements.getAttribute("ver");
522 if (version != null) {
523 account.getRoster().setVersion(version);
524 }
525 for (Element item : elements.getChildren()) {
526 if (item.getName().equals("item")) {
527 String jid = item.getAttribute("jid");
528 String name = item.getAttribute("name");
529 String subscription = item.getAttribute("subscription");
530 Contact contact = account.getRoster().getContact(jid);
531 if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
532 contact.setServerName(name);
533 }
534 if (subscription.equals("remove")) {
535 contact.resetOption(Contact.Options.IN_ROSTER);
536 contact.resetOption(Contact.Options.DIRTY_DELETE);
537 } else {
538 contact.setOption(Contact.Options.IN_ROSTER);
539 contact.parseSubscriptionFromElement(item);
540 }
541 }
542 }
543 }
544
545 public class XmppConnectionBinder extends Binder {
546 public XmppConnectionService getService() {
547 return XmppConnectionService.this;
548 }
549 }
550
551 @Override
552 public int onStartCommand(Intent intent, int flags, int startId) {
553 if ((intent != null)
554 && (ACTION_MERGE_PHONE_CONTACTS.equals(intent.getAction()))) {
555 mergePhoneContactsWithRoster();
556 return START_STICKY;
557 } else if ((intent != null)
558 && (Intent.ACTION_SHUTDOWN.equals(intent.getAction()))) {
559 logoutAndSave();
560 return START_NOT_STICKY;
561 }
562 this.wakeLock.acquire();
563 ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
564 .getSystemService(Context.CONNECTIVITY_SERVICE);
565 NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
566 boolean isConnected = activeNetwork != null
567 && activeNetwork.isConnected();
568
569 for (Account account : accounts) {
570 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
571 if (!isConnected) {
572 account.setStatus(Account.STATUS_NO_INTERNET);
573 if (statusListener != null) {
574 statusListener.onStatusChanged(account);
575 }
576 } else {
577 if (account.getStatus() == Account.STATUS_NO_INTERNET) {
578 account.setStatus(Account.STATUS_OFFLINE);
579 if (statusListener != null) {
580 statusListener.onStatusChanged(account);
581 }
582 }
583 if (account.getStatus() == Account.STATUS_ONLINE) {
584 long lastReceived = account.getXmppConnection().lastPaketReceived;
585 long lastSent = account.getXmppConnection().lastPingSent;
586 if (lastSent - lastReceived >= PING_TIMEOUT * 1000) {
587 Log.d(LOGTAG, account.getJid() + ": ping timeout");
588 this.reconnectAccount(account, true);
589 } else if (SystemClock.elapsedRealtime() - lastReceived >= PING_MIN_INTERVAL * 1000) {
590 account.getXmppConnection().sendPing();
591 account.getXmppConnection().lastPingSent = SystemClock
592 .elapsedRealtime();
593 this.scheduleWakeupCall(2, false);
594 }
595 } else if (account.getStatus() == Account.STATUS_OFFLINE) {
596 if (account.getXmppConnection() == null) {
597 account.setXmppConnection(this
598 .createConnection(account));
599 }
600 account.getXmppConnection().lastPingSent = SystemClock
601 .elapsedRealtime();
602 new Thread(account.getXmppConnection()).start();
603 } else if ((account.getStatus() == Account.STATUS_CONNECTING)
604 && ((SystemClock.elapsedRealtime() - account
605 .getXmppConnection().lastConnect) / 1000 >= CONNECT_TIMEOUT)) {
606 Log.d(LOGTAG, account.getJid()
607 + ": time out during connect reconnecting");
608 reconnectAccount(account, true);
609 } else {
610 if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
611 reconnectAccount(account, true);
612 }
613 }
614 // in any case. reschedule wakup call
615 this.scheduleWakeupCall(PING_MAX_INTERVAL, true);
616 }
617 if (accountChangedListener != null) {
618 accountChangedListener.onAccountListChangedListener();
619 }
620 }
621 }
622 if (wakeLock.isHeld()) {
623 wakeLock.release();
624 }
625 return START_STICKY;
626 }
627
628 @Override
629 public void onCreate() {
630 ExceptionHelper.init(getApplicationContext());
631 this.databaseBackend = DatabaseBackend
632 .getInstance(getApplicationContext());
633 this.fileBackend = new FileBackend(getApplicationContext());
634 this.accounts = databaseBackend.getAccounts();
635
636 for (Account account : this.accounts) {
637 this.databaseBackend.readRoster(account.getRoster());
638 }
639 this.mergePhoneContactsWithRoster();
640 this.getConversations();
641
642 getContentResolver().registerContentObserver(
643 ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
644 this.pgpServiceConnection = new OpenPgpServiceConnection(
645 getApplicationContext(), "org.sufficientlysecure.keychain");
646 this.pgpServiceConnection.bindToService();
647
648 this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
649 this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
650 "XmppConnectionService");
651 }
652
653 @Override
654 public void onDestroy() {
655 super.onDestroy();
656 this.logoutAndSave();
657 }
658
659 @Override
660 public void onTaskRemoved(Intent rootIntent) {
661 super.onTaskRemoved(rootIntent);
662 this.logoutAndSave();
663 }
664
665 private void logoutAndSave() {
666 for (Account account : accounts) {
667 databaseBackend.writeRoster(account.getRoster());
668 if (account.getXmppConnection() != null) {
669 disconnect(account, false);
670 }
671 }
672 Context context = getApplicationContext();
673 AlarmManager alarmManager = (AlarmManager) context
674 .getSystemService(Context.ALARM_SERVICE);
675 Intent intent = new Intent(context, EventReceiver.class);
676 alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
677 Log.d(LOGTAG, "good bye");
678 stopSelf();
679 }
680
681 protected void scheduleWakeupCall(int seconds, boolean ping) {
682 long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000;
683 Context context = getApplicationContext();
684 AlarmManager alarmManager = (AlarmManager) context
685 .getSystemService(Context.ALARM_SERVICE);
686
687 if (ping) {
688 if (this.pingIntent == null) {
689 this.pingIntent = new Intent(context, EventReceiver.class);
690 this.pingIntent.setAction("ping");
691 this.pingIntent.putExtra("time", timeToWake);
692 this.pendingPingIntent = PendingIntent.getBroadcast(context, 0,
693 this.pingIntent, 0);
694 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
695 timeToWake, pendingPingIntent);
696 } else {
697 long scheduledTime = this.pingIntent.getLongExtra("time", 0);
698 if (scheduledTime < SystemClock.elapsedRealtime()
699 || (scheduledTime > timeToWake)) {
700 this.pingIntent.putExtra("time", timeToWake);
701 alarmManager.cancel(this.pendingPingIntent);
702 this.pendingPingIntent = PendingIntent.getBroadcast(
703 context, 0, this.pingIntent, 0);
704 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
705 timeToWake, pendingPingIntent);
706 }
707 }
708 } else {
709 Intent intent = new Intent(context, EventReceiver.class);
710 intent.setAction("ping_check");
711 PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0,
712 intent, 0);
713 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake,
714 alarmIntent);
715 }
716
717 }
718
719 public XmppConnection createConnection(Account account) {
720 SharedPreferences sharedPref = getPreferences();
721 account.setResource(sharedPref.getString("resource", "mobile")
722 .toLowerCase(Locale.getDefault()));
723 XmppConnection connection = new XmppConnection(account, this.pm);
724 connection.setOnMessagePacketReceivedListener(this.messageListener);
725 connection.setOnStatusChangedListener(this.statusListener);
726 connection.setOnPresencePacketReceivedListener(this.presenceListener);
727 connection
728 .setOnUnregisteredIqPacketReceivedListener(this.unknownIqListener);
729 connection.setOnJinglePacketReceivedListener(this.jingleListener);
730 connection
731 .setOnTLSExceptionReceivedListener(new OnTLSExceptionReceived() {
732
733 @Override
734 public void onTLSExceptionReceived(String fingerprint,
735 Account account) {
736 Log.d(LOGTAG, "tls exception arrived in service");
737 if (tlsException != null) {
738 tlsException.onTLSExceptionReceived(fingerprint,
739 account);
740 }
741 }
742 });
743 connection.setOnBindListener(new OnBindListener() {
744
745 @Override
746 public void onBind(final Account account) {
747 account.getRoster().clearPresences();
748 account.clearPresences(); // self presences
749 fetchRosterFromServer(account);
750 sendPresence(account);
751 connectMultiModeConversations(account);
752 if (convChangedListener != null) {
753 convChangedListener.onConversationListChanged();
754 }
755 }
756 });
757 return connection;
758 }
759
760 synchronized public void sendMessage(Message message, String presence) {
761 Account account = message.getConversation().getAccount();
762 Conversation conv = message.getConversation();
763 MessagePacket packet = null;
764 boolean saveInDb = false;
765 boolean addToConversation = false;
766 boolean send = false;
767 if (account.getStatus() == Account.STATUS_ONLINE) {
768 if (message.getType() == Message.TYPE_IMAGE) {
769 mJingleConnectionManager.createNewConnection(message);
770 } else {
771 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
772 if (!conv.hasValidOtrSession()) {
773 // starting otr session. messages will be send later
774 conv.startOtrSession(getApplicationContext(), presence,
775 true);
776 } else if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
777 // otr session aleary exists, creating message packet
778 // accordingly
779 packet = prepareMessagePacket(account, message,
780 conv.getOtrSession());
781 send = true;
782 message.setStatus(Message.STATUS_SEND);
783 }
784 saveInDb = true;
785 addToConversation = true;
786 } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
787 message.getConversation().endOtrIfNeeded();
788 packet = prepareMessagePacket(account, message, null);
789 packet.setBody("This is an XEP-0027 encryted message");
790 packet.addChild("x", "jabber:x:encrypted").setContent(
791 message.getEncryptedBody());
792 message.setStatus(Message.STATUS_SEND);
793 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
794 saveInDb = true;
795 addToConversation = true;
796 send = true;
797 } else {
798 message.getConversation().endOtrIfNeeded();
799 // don't encrypt
800 if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
801 message.setStatus(Message.STATUS_SEND);
802 }
803 packet = prepareMessagePacket(account, message, null);
804 send = true;
805 saveInDb = true;
806 addToConversation = true;
807 }
808 }
809 } else {
810 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
811 String pgpBody = message.getEncryptedBody();
812 String decryptedBody = message.getBody();
813 message.setBody(pgpBody);
814 databaseBackend.createMessage(message);
815 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
816 message.setBody(decryptedBody);
817 addToConversation = true;
818 } else {
819 saveInDb = true;
820 addToConversation = true;
821 }
822
823 }
824 if (saveInDb) {
825 databaseBackend.createMessage(message);
826 }
827 if (addToConversation) {
828 conv.getMessages().add(message);
829 if (convChangedListener != null) {
830 convChangedListener.onConversationListChanged();
831 }
832 }
833 if ((send) && (packet != null)) {
834 account.getXmppConnection().sendMessagePacket(packet);
835 }
836
837 }
838
839 private void sendUnsendMessages(Conversation conversation) {
840 for (int i = 0; i < conversation.getMessages().size(); ++i) {
841 if (conversation.getMessages().get(i).getStatus() == Message.STATUS_UNSEND) {
842 resendMessage(conversation.getMessages().get(i));
843 }
844 }
845 }
846
847 private void resendMessage(Message message) {
848 Account account = message.getConversation().getAccount();
849 MessagePacket packet = null;
850 if (message.getEncryption() == Message.ENCRYPTION_NONE) {
851 packet = prepareMessagePacket(account, message, null);
852 } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
853 packet = prepareMessagePacket(account, message, null);
854 packet.setBody("This is an XEP-0027 encryted message");
855 if (message.getEncryptedBody() == null) {
856 markMessage(message, Message.STATUS_SEND_FAILED);
857 return;
858 }
859 packet.addChild("x", "jabber:x:encrypted").setContent(
860 message.getEncryptedBody());
861 } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
862 packet = prepareMessagePacket(account, message, null);
863 packet.setBody("This is an XEP-0027 encryted message");
864 packet.addChild("x", "jabber:x:encrypted").setContent(
865 message.getBody());
866 }
867 if (packet != null) {
868 account.getXmppConnection().sendMessagePacket(packet);
869 markMessage(message, Message.STATUS_SEND);
870 }
871 }
872
873 public MessagePacket prepareMessagePacket(Account account, Message message,
874 Session otrSession) {
875 MessagePacket packet = new MessagePacket();
876 if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
877 packet.setType(MessagePacket.TYPE_CHAT);
878 if (otrSession != null) {
879 try {
880 packet.setBody(otrSession.transformSending(message
881 .getBody()));
882 } catch (OtrException e) {
883 Log.d(LOGTAG,
884 account.getJid()
885 + ": could not encrypt message to "
886 + message.getCounterpart());
887 }
888 packet.addChild("private", "urn:xmpp:carbons:2");
889 packet.addChild("no-copy", "urn:xmpp:hints");
890 packet.setTo(otrSession.getSessionID().getAccountID() + "/"
891 + otrSession.getSessionID().getUserID());
892 packet.setFrom(account.getFullJid());
893 } else {
894 packet.setBody(message.getBody());
895 packet.setTo(message.getCounterpart());
896 packet.setFrom(account.getJid());
897 }
898 packet.addChild("markable", "urn:xmpp:chat-markers:0");
899 } else if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
900 packet.setType(MessagePacket.TYPE_GROUPCHAT);
901 packet.setBody(message.getBody());
902 packet.setTo(message.getCounterpart().split("/")[0]);
903 packet.setFrom(account.getJid());
904 }
905 packet.setId(message.getUuid());
906 return packet;
907 }
908
909 public void fetchRosterFromServer(Account account) {
910 IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
911 if (!"".equals(account.getRosterVersion())) {
912 Log.d(LOGTAG, account.getJid() + ": fetching roster version "
913 + account.getRosterVersion());
914 } else {
915 Log.d(LOGTAG, account.getJid() + ": fetching roster");
916 }
917 iqPacket.query("jabber:iq:roster").setAttribute("ver",
918 account.getRosterVersion());
919 account.getXmppConnection().sendIqPacket(iqPacket,
920 new OnIqPacketReceived() {
921
922 @Override
923 public void onIqPacketReceived(final Account account,
924 IqPacket packet) {
925 Element roster = packet.findChild("query");
926 if (roster != null) {
927 account.getRoster().markAllAsNotInRoster();
928 processRosterItems(account, roster);
929 }
930 }
931 });
932 }
933
934 private void mergePhoneContactsWithRoster() {
935 PhoneHelper.loadPhoneContacts(getApplicationContext(),
936 new OnPhoneContactsLoadedListener() {
937 @Override
938 public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
939 for (Account account : accounts) {
940 account.getRoster().clearSystemAccounts();
941 }
942 for (Bundle phoneContact : phoneContacts) {
943 for (Account account : accounts) {
944 String jid = phoneContact.getString("jid");
945 Contact contact = account.getRoster()
946 .getContact(jid);
947 String systemAccount = phoneContact
948 .getInt("phoneid")
949 + "#"
950 + phoneContact.getString("lookup");
951 contact.setSystemAccount(systemAccount);
952 contact.setPhotoUri(phoneContact
953 .getString("photouri"));
954 contact.setSystemName(phoneContact
955 .getString("displayname"));
956 }
957 }
958 }
959 });
960 }
961
962 public List<Conversation> getConversations() {
963 if (this.conversations == null) {
964 Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
965 for (Account account : this.accounts) {
966 accountLookupTable.put(account.getUuid(), account);
967 }
968 this.conversations = databaseBackend
969 .getConversations(Conversation.STATUS_AVAILABLE);
970 for (Conversation conv : this.conversations) {
971 Account account = accountLookupTable.get(conv.getAccountUuid());
972 conv.setAccount(account);
973 conv.setMessages(databaseBackend.getMessages(conv, 50));
974 }
975 }
976 Collections.sort(this.conversations, new Comparator<Conversation>() {
977 @Override
978 public int compare(Conversation lhs, Conversation rhs) {
979 Message left = lhs.getLatestMessage();
980 Message right = rhs.getLatestMessage();
981 if (left.getTimeSent() > right.getTimeSent()) {
982 return -1;
983 } else if (left.getTimeSent() < right.getTimeSent()) {
984 return 1;
985 } else {
986 return 0;
987 }
988 }
989 });
990 return this.conversations;
991 }
992
993 public List<Account> getAccounts() {
994 return this.accounts;
995 }
996
997 public Conversation findActiveConversation(Contact contact) {
998 for (Conversation conversation : this.getConversations()) {
999 if (conversation.getContact() == contact) {
1000 return conversation;
1001 }
1002 }
1003 return null;
1004 }
1005
1006 public Conversation findOrCreateConversation(Account account, String jid,
1007 boolean muc) {
1008 for (Conversation conv : this.getConversations()) {
1009 if ((conv.getAccount().equals(account))
1010 && (conv.getContactJid().split("/")[0].equals(jid))) {
1011 return conv;
1012 }
1013 }
1014 Conversation conversation = databaseBackend.findConversation(account,
1015 jid);
1016 if (conversation != null) {
1017 conversation.setStatus(Conversation.STATUS_AVAILABLE);
1018 conversation.setAccount(account);
1019 if (muc) {
1020 conversation.setMode(Conversation.MODE_MULTI);
1021 } else {
1022 conversation.setMode(Conversation.MODE_SINGLE);
1023 }
1024 conversation.setMessages(databaseBackend.getMessages(conversation,
1025 50));
1026 this.databaseBackend.updateConversation(conversation);
1027 } else {
1028 String conversationName;
1029 Contact contact = account.getRoster().getContact(jid);
1030 if (contact != null) {
1031 conversationName = contact.getDisplayName();
1032 } else {
1033 conversationName = jid.split("@")[0];
1034 }
1035 if (muc) {
1036 conversation = new Conversation(conversationName, account, jid,
1037 Conversation.MODE_MULTI);
1038 } else {
1039 conversation = new Conversation(conversationName, account, jid,
1040 Conversation.MODE_SINGLE);
1041 }
1042 this.databaseBackend.createConversation(conversation);
1043 }
1044 this.conversations.add(conversation);
1045 if ((account.getStatus() == Account.STATUS_ONLINE)
1046 && (conversation.getMode() == Conversation.MODE_MULTI)) {
1047 joinMuc(conversation);
1048 }
1049 if (this.convChangedListener != null) {
1050 this.convChangedListener.onConversationListChanged();
1051 }
1052 return conversation;
1053 }
1054
1055 public void archiveConversation(Conversation conversation) {
1056 if (conversation.getMode() == Conversation.MODE_MULTI) {
1057 leaveMuc(conversation);
1058 } else {
1059 conversation.endOtrIfNeeded();
1060 }
1061 this.databaseBackend.updateConversation(conversation);
1062 this.conversations.remove(conversation);
1063 if (this.convChangedListener != null) {
1064 this.convChangedListener.onConversationListChanged();
1065 }
1066 }
1067
1068 public void clearConversationHistory(Conversation conversation) {
1069 this.databaseBackend.deleteMessagesInConversation(conversation);
1070 this.fileBackend.removeFiles(conversation);
1071 conversation.getMessages().clear();
1072 if (this.convChangedListener != null) {
1073 this.convChangedListener.onConversationListChanged();
1074 }
1075 }
1076
1077 public int getConversationCount() {
1078 return this.databaseBackend.getConversationCount();
1079 }
1080
1081 public void createAccount(Account account) {
1082 databaseBackend.createAccount(account);
1083 this.accounts.add(account);
1084 this.reconnectAccount(account, false);
1085 if (accountChangedListener != null)
1086 accountChangedListener.onAccountListChangedListener();
1087 }
1088
1089 public void updateAccount(Account account) {
1090 this.statusListener.onStatusChanged(account);
1091 databaseBackend.updateAccount(account);
1092 reconnectAccount(account, false);
1093 if (accountChangedListener != null) {
1094 accountChangedListener.onAccountListChangedListener();
1095 }
1096 UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
1097 }
1098
1099 public void deleteAccount(Account account) {
1100 if (account.getXmppConnection() != null) {
1101 this.disconnect(account, true);
1102 }
1103 databaseBackend.deleteAccount(account);
1104 this.accounts.remove(account);
1105 if (accountChangedListener != null) {
1106 accountChangedListener.onAccountListChangedListener();
1107 }
1108 UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
1109 }
1110
1111 public void setOnConversationListChangedListener(
1112 OnConversationListChangedListener listener) {
1113 this.convChangedListener = listener;
1114 this.convChangedListenerCount++;
1115 }
1116
1117 public void removeOnConversationListChangedListener() {
1118 this.convChangedListenerCount--;
1119 if (this.convChangedListenerCount == 0) {
1120 this.convChangedListener = null;
1121 }
1122 }
1123
1124 public void setOnAccountListChangedListener(
1125 OnAccountListChangedListener listener) {
1126 this.accountChangedListener = listener;
1127 }
1128
1129 public void removeOnAccountListChangedListener() {
1130 this.accountChangedListener = null;
1131 }
1132
1133 public void connectMultiModeConversations(Account account) {
1134 List<Conversation> conversations = getConversations();
1135 for (int i = 0; i < conversations.size(); i++) {
1136 Conversation conversation = conversations.get(i);
1137 if ((conversation.getMode() == Conversation.MODE_MULTI)
1138 && (conversation.getAccount() == account)) {
1139 joinMuc(conversation);
1140 }
1141 }
1142 }
1143
1144 public void joinMuc(Conversation conversation) {
1145 Account account = conversation.getAccount();
1146 String[] mucParts = conversation.getContactJid().split("/");
1147 String muc;
1148 String nick;
1149 if (mucParts.length == 2) {
1150 muc = mucParts[0];
1151 nick = mucParts[1];
1152 } else {
1153 muc = mucParts[0];
1154 nick = account.getUsername();
1155 }
1156 PresencePacket packet = new PresencePacket();
1157 packet.setAttribute("to", muc + "/" + nick);
1158 Element x = new Element("x");
1159 x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
1160 String sig = account.getPgpSignature();
1161 if (sig != null) {
1162 packet.addChild("status").setContent("online");
1163 packet.addChild("x", "jabber:x:signed").setContent(sig);
1164 }
1165 if (conversation.getMessages().size() != 0) {
1166 long lastMsgTime = conversation.getLatestMessage().getTimeSent();
1167 long diff = (System.currentTimeMillis() - lastMsgTime) / 1000 - 1;
1168 x.addChild("history").setAttribute("seconds", diff + "");
1169 }
1170 packet.addChild(x);
1171 account.getXmppConnection().sendPresencePacket(packet);
1172 }
1173
1174 private OnRenameListener renameListener = null;
1175
1176 public void setOnRenameListener(OnRenameListener listener) {
1177 this.renameListener = listener;
1178 }
1179
1180 public void renameInMuc(final Conversation conversation, final String nick) {
1181 final MucOptions options = conversation.getMucOptions();
1182 if (options.online()) {
1183 Account account = conversation.getAccount();
1184 options.setOnRenameListener(new OnRenameListener() {
1185
1186 @Override
1187 public void onRename(boolean success) {
1188 if (renameListener != null) {
1189 renameListener.onRename(success);
1190 }
1191 if (success) {
1192 String jid = conversation.getContactJid().split("/")[0]
1193 + "/" + nick;
1194 conversation.setContactJid(jid);
1195 databaseBackend.updateConversation(conversation);
1196 }
1197 }
1198 });
1199 options.flagAboutToRename();
1200 PresencePacket packet = new PresencePacket();
1201 packet.setAttribute("to",
1202 conversation.getContactJid().split("/")[0] + "/" + nick);
1203 packet.setAttribute("from", conversation.getAccount().getFullJid());
1204
1205 String sig = account.getPgpSignature();
1206 if (sig != null) {
1207 packet.addChild("status").setContent("online");
1208 packet.addChild("x", "jabber:x:signed").setContent(sig);
1209 }
1210
1211 account.getXmppConnection().sendPresencePacket(packet, null);
1212 } else {
1213 String jid = conversation.getContactJid().split("/")[0] + "/"
1214 + nick;
1215 conversation.setContactJid(jid);
1216 databaseBackend.updateConversation(conversation);
1217 if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
1218 joinMuc(conversation);
1219 }
1220 }
1221 }
1222
1223 public void leaveMuc(Conversation conversation) {
1224 PresencePacket packet = new PresencePacket();
1225 packet.setAttribute("to", conversation.getContactJid().split("/")[0]
1226 + "/" + conversation.getMucOptions().getNick());
1227 packet.setAttribute("from", conversation.getAccount().getFullJid());
1228 packet.setAttribute("type", "unavailable");
1229 Log.d(LOGTAG, "send leaving muc " + packet);
1230 conversation.getAccount().getXmppConnection()
1231 .sendPresencePacket(packet);
1232 conversation.getMucOptions().setOffline();
1233 }
1234
1235 public void disconnect(Account account, boolean force) {
1236 if ((account.getStatus() == Account.STATUS_ONLINE)
1237 || (account.getStatus() == Account.STATUS_DISABLED)) {
1238 if (!force) {
1239 List<Conversation> conversations = getConversations();
1240 for (int i = 0; i < conversations.size(); i++) {
1241 Conversation conversation = conversations.get(i);
1242 if (conversation.getAccount() == account) {
1243 if (conversation.getMode() == Conversation.MODE_MULTI) {
1244 leaveMuc(conversation);
1245 } else {
1246 conversation.endOtrIfNeeded();
1247 }
1248 }
1249 }
1250 }
1251 account.getXmppConnection().disconnect(force);
1252 }
1253 }
1254
1255 @Override
1256 public IBinder onBind(Intent intent) {
1257 return mBinder;
1258 }
1259
1260 public void updateMessage(Message message) {
1261 databaseBackend.updateMessage(message);
1262 }
1263
1264 protected void syncDirtyContacts(Account account) {
1265 for (Contact contact : account.getRoster().getContacts()) {
1266 if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
1267 pushContactToServer(contact);
1268 }
1269 if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
1270 Log.d(LOGTAG, "dirty delete");
1271 deleteContactOnServer(contact);
1272 }
1273 }
1274 }
1275
1276 public void createContact(Contact contact) {
1277 SharedPreferences sharedPref = getPreferences();
1278 boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1279 if (autoGrant) {
1280 contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
1281 contact.setOption(Contact.Options.ASKING);
1282 }
1283 pushContactToServer(contact);
1284 }
1285
1286 public void pushContactToServer(Contact contact) {
1287 contact.resetOption(Contact.Options.DIRTY_DELETE);
1288 Account account = contact.getAccount();
1289 if (account.getStatus() == Account.STATUS_ONLINE) {
1290 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1291 iq.query("jabber:iq:roster").addChild(contact.asElement());
1292 account.getXmppConnection().sendIqPacket(iq, null);
1293 if (contact.getOption(Contact.Options.ASKING)) {
1294 requestPresenceUpdatesFrom(contact);
1295 }
1296 if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1297 Log.d("xmppService", "contact had pending subscription");
1298 sendPresenceUpdatesTo(contact);
1299 }
1300 contact.resetOption(Contact.Options.DIRTY_PUSH);
1301 } else {
1302 contact.setOption(Contact.Options.DIRTY_PUSH);
1303 }
1304 }
1305
1306 public void deleteContactOnServer(Contact contact) {
1307 contact.resetOption(Contact.Options.DIRTY_PUSH);
1308 Account account = contact.getAccount();
1309 if (account.getStatus() == Account.STATUS_ONLINE) {
1310 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1311 Element item = iq.query("jabber:iq:roster").addChild("item");
1312 item.setAttribute("jid", contact.getJid());
1313 item.setAttribute("subscription", "remove");
1314 account.getXmppConnection().sendIqPacket(iq, null);
1315 contact.resetOption(Contact.Options.DIRTY_DELETE);
1316 } else {
1317 contact.setOption(Contact.Options.DIRTY_DELETE);
1318 }
1319 }
1320
1321 public void requestPresenceUpdatesFrom(Contact contact) {
1322 PresencePacket packet = new PresencePacket();
1323 packet.setAttribute("type", "subscribe");
1324 packet.setAttribute("to", contact.getJid());
1325 packet.setAttribute("from", contact.getAccount().getJid());
1326 contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1327 }
1328
1329 public void stopPresenceUpdatesFrom(Contact contact) {
1330 PresencePacket packet = new PresencePacket();
1331 packet.setAttribute("type", "unsubscribe");
1332 packet.setAttribute("to", contact.getJid());
1333 packet.setAttribute("from", contact.getAccount().getJid());
1334 contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1335 }
1336
1337 public void stopPresenceUpdatesTo(Contact contact) {
1338 PresencePacket packet = new PresencePacket();
1339 packet.setAttribute("type", "unsubscribed");
1340 packet.setAttribute("to", contact.getJid());
1341 packet.setAttribute("from", contact.getAccount().getJid());
1342 contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1343 }
1344
1345 public void sendPresenceUpdatesTo(Contact contact) {
1346 PresencePacket packet = new PresencePacket();
1347 packet.setAttribute("type", "subscribed");
1348 packet.setAttribute("to", contact.getJid());
1349 packet.setAttribute("from", contact.getAccount().getJid());
1350 contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1351 }
1352
1353 public void sendPresence(Account account) {
1354 PresencePacket packet = new PresencePacket();
1355 packet.setAttribute("from", account.getFullJid());
1356 String sig = account.getPgpSignature();
1357 if (sig != null) {
1358 packet.addChild("status").setContent("online");
1359 packet.addChild("x", "jabber:x:signed").setContent(sig);
1360 }
1361 account.getXmppConnection().sendPresencePacket(packet);
1362 }
1363
1364 public void updateConversation(Conversation conversation) {
1365 this.databaseBackend.updateConversation(conversation);
1366 }
1367
1368 public void removeOnTLSExceptionReceivedListener() {
1369 this.tlsException = null;
1370 }
1371
1372 public void reconnectAccount(final Account account, final boolean force) {
1373 new Thread(new Runnable() {
1374
1375 @Override
1376 public void run() {
1377 if (account.getXmppConnection() != null) {
1378 disconnect(account, force);
1379 }
1380 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1381 if (account.getXmppConnection() == null) {
1382 account.setXmppConnection(createConnection(account));
1383 }
1384 Thread thread = new Thread(account.getXmppConnection());
1385 thread.start();
1386 scheduleWakeupCall((int) (CONNECT_TIMEOUT * 1.2), false);
1387 }
1388 }
1389 }).start();
1390 }
1391
1392 public void sendConversationSubject(Conversation conversation,
1393 String subject) {
1394 MessagePacket packet = new MessagePacket();
1395 packet.setType(MessagePacket.TYPE_GROUPCHAT);
1396 packet.setTo(conversation.getContactJid().split("/")[0]);
1397 Element subjectChild = new Element("subject");
1398 subjectChild.setContent(subject);
1399 packet.addChild(subjectChild);
1400 packet.setFrom(conversation.getAccount().getJid());
1401 Account account = conversation.getAccount();
1402 if (account.getStatus() == Account.STATUS_ONLINE) {
1403 account.getXmppConnection().sendMessagePacket(packet);
1404 }
1405 }
1406
1407 public void inviteToConference(Conversation conversation,
1408 List<Contact> contacts) {
1409 for (Contact contact : contacts) {
1410 MessagePacket packet = new MessagePacket();
1411 packet.setTo(conversation.getContactJid().split("/")[0]);
1412 packet.setFrom(conversation.getAccount().getFullJid());
1413 Element x = new Element("x");
1414 x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
1415 Element invite = new Element("invite");
1416 invite.setAttribute("to", contact.getJid());
1417 x.addChild(invite);
1418 packet.addChild(x);
1419 Log.d(LOGTAG, packet.toString());
1420 conversation.getAccount().getXmppConnection()
1421 .sendMessagePacket(packet);
1422 }
1423
1424 }
1425
1426 public boolean markMessage(Account account, String recipient, String uuid,
1427 int status) {
1428 for (Conversation conversation : getConversations()) {
1429 if (conversation.getContactJid().equals(recipient)
1430 && conversation.getAccount().equals(account)) {
1431 return markMessage(conversation, uuid, status);
1432 }
1433 }
1434 return false;
1435 }
1436
1437 public boolean markMessage(Conversation conversation, String uuid,
1438 int status) {
1439 for (Message message : conversation.getMessages()) {
1440 if (message.getUuid().equals(uuid)) {
1441 markMessage(message, status);
1442 return true;
1443 }
1444 }
1445 return false;
1446 }
1447
1448 public void markMessage(Message message, int status) {
1449 message.setStatus(status);
1450 databaseBackend.updateMessage(message);
1451 if (convChangedListener != null) {
1452 convChangedListener.onConversationListChanged();
1453 }
1454 }
1455
1456 public SharedPreferences getPreferences() {
1457 return PreferenceManager
1458 .getDefaultSharedPreferences(getApplicationContext());
1459 }
1460
1461 public boolean confirmMessages() {
1462 return getPreferences().getBoolean("confirm_messages", true);
1463 }
1464
1465 public void updateUi(Conversation conversation, boolean notify) {
1466 if (convChangedListener != null) {
1467 convChangedListener.onConversationListChanged();
1468 } else {
1469 UIHelper.updateNotification(getApplicationContext(),
1470 getConversations(), conversation, notify);
1471 }
1472 }
1473
1474 public Account findAccountByJid(String accountJid) {
1475 for (Account account : this.accounts) {
1476 if (account.getJid().equals(accountJid)) {
1477 return account;
1478 }
1479 }
1480 return null;
1481 }
1482
1483 public void markRead(Conversation conversation) {
1484 conversation.markRead(this);
1485 }
1486
1487 public void sendConfirmMessage(Account account, String to, String id) {
1488 MessagePacket receivedPacket = new MessagePacket();
1489 receivedPacket.setType(MessagePacket.TYPE_NORMAL);
1490 receivedPacket.setTo(to);
1491 receivedPacket.setFrom(account.getFullJid());
1492 Element received = receivedPacket.addChild("displayed",
1493 "urn:xmpp:chat-markers:0");
1494 received.setAttribute("id", id);
1495 account.getXmppConnection().sendMessagePacket(receivedPacket);
1496 }
1497}