1package eu.siacs.conversations.services;
2
3import java.security.SecureRandom;
4import java.text.SimpleDateFormat;
5import java.util.ArrayList;
6import java.util.Collections;
7import java.util.Comparator;
8import java.util.Date;
9import java.util.Hashtable;
10import java.util.List;
11import java.util.Locale;
12import java.util.TimeZone;
13import java.util.concurrent.CopyOnWriteArrayList;
14
15import org.openintents.openpgp.util.OpenPgpApi;
16import org.openintents.openpgp.util.OpenPgpServiceConnection;
17
18import de.duenndns.ssl.MemorizingTrustManager;
19
20import net.java.otr4j.OtrException;
21import net.java.otr4j.session.Session;
22import net.java.otr4j.session.SessionStatus;
23import eu.siacs.conversations.R;
24import eu.siacs.conversations.crypto.PgpEngine;
25import eu.siacs.conversations.entities.Account;
26import eu.siacs.conversations.entities.Bookmark;
27import eu.siacs.conversations.entities.Contact;
28import eu.siacs.conversations.entities.Conversation;
29import eu.siacs.conversations.entities.Message;
30import eu.siacs.conversations.entities.MucOptions;
31import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
32import eu.siacs.conversations.entities.Presences;
33import eu.siacs.conversations.generator.IqGenerator;
34import eu.siacs.conversations.generator.MessageGenerator;
35import eu.siacs.conversations.generator.PresenceGenerator;
36import eu.siacs.conversations.parser.IqParser;
37import eu.siacs.conversations.parser.MessageParser;
38import eu.siacs.conversations.parser.PresenceParser;
39import eu.siacs.conversations.persistance.DatabaseBackend;
40import eu.siacs.conversations.persistance.FileBackend;
41import eu.siacs.conversations.ui.UiCallback;
42import eu.siacs.conversations.utils.CryptoHelper;
43import eu.siacs.conversations.utils.ExceptionHelper;
44import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
45import eu.siacs.conversations.utils.PRNGFixes;
46import eu.siacs.conversations.utils.PhoneHelper;
47import eu.siacs.conversations.utils.UIHelper;
48import eu.siacs.conversations.xml.Element;
49import eu.siacs.conversations.xmpp.OnBindListener;
50import eu.siacs.conversations.xmpp.OnContactStatusChanged;
51import eu.siacs.conversations.xmpp.OnIqPacketReceived;
52import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
53import eu.siacs.conversations.xmpp.OnStatusChanged;
54import eu.siacs.conversations.xmpp.XmppConnection;
55import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
56import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
57import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
58import eu.siacs.conversations.xmpp.pep.Avatar;
59import eu.siacs.conversations.xmpp.stanzas.IqPacket;
60import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
61import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
62import android.annotation.SuppressLint;
63import android.app.AlarmManager;
64import android.app.PendingIntent;
65import android.app.Service;
66import android.content.Context;
67import android.content.Intent;
68import android.content.SharedPreferences;
69import android.database.ContentObserver;
70import android.graphics.Bitmap;
71import android.net.ConnectivityManager;
72import android.net.NetworkInfo;
73import android.net.Uri;
74import android.os.Binder;
75import android.os.Bundle;
76import android.os.IBinder;
77import android.os.PowerManager;
78import android.os.PowerManager.WakeLock;
79import android.os.SystemClock;
80import android.preference.PreferenceManager;
81import android.provider.ContactsContract;
82import android.util.Log;
83
84public class XmppConnectionService extends Service {
85
86 protected static final String LOGTAG = "xmppService";
87 public DatabaseBackend databaseBackend;
88 private FileBackend fileBackend;
89
90 public long startDate;
91
92 private static final int PING_MAX_INTERVAL = 300;
93 private static final int PING_MIN_INTERVAL = 30;
94 private static final int PING_TIMEOUT = 10;
95 private static final int CONNECT_TIMEOUT = 90;
96 public static final long CARBON_GRACE_PERIOD = 60000L;
97
98 private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
99
100 private MemorizingTrustManager mMemorizingTrustManager;
101
102 private MessageParser mMessageParser = new MessageParser(this);
103 private PresenceParser mPresenceParser = new PresenceParser(this);
104 private IqParser mIqParser = new IqParser(this);
105 private MessageGenerator mMessageGenerator = new MessageGenerator();
106 private PresenceGenerator mPresenceGenerator = new PresenceGenerator();
107
108 private List<Account> accounts;
109 private CopyOnWriteArrayList<Conversation> conversations = null;
110 private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
111 this);
112
113 private OnConversationUpdate mOnConversationUpdate = null;
114 private int convChangedListenerCount = 0;
115 private OnAccountUpdate mOnAccountUpdate = null;
116 private int accountChangedListenerCount = 0;
117 private OnRosterUpdate mOnRosterUpdate = null;
118 public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
119
120 @Override
121 public void onContactStatusChanged(Contact contact, boolean online) {
122 Conversation conversation = find(getConversations(), contact);
123 if (conversation != null) {
124 conversation.endOtrIfNeeded();
125 if (online && (contact.getPresences().size() == 1)) {
126 sendUnsendMessages(conversation);
127 }
128 }
129 }
130 };
131
132 private SecureRandom mRandom;
133
134 private ContentObserver contactObserver = new ContentObserver(null) {
135 @Override
136 public void onChange(boolean selfChange) {
137 super.onChange(selfChange);
138 Intent intent = new Intent(getApplicationContext(),
139 XmppConnectionService.class);
140 intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
141 startService(intent);
142 }
143 };
144
145 private final IBinder mBinder = new XmppConnectionBinder();
146 private OnStatusChanged statusListener = new OnStatusChanged() {
147
148 @Override
149 public void onStatusChanged(Account account) {
150 if (mOnAccountUpdate != null) {
151 mOnAccountUpdate.onAccountUpdate();
152 ;
153 }
154 if (account.getStatus() == Account.STATUS_ONLINE) {
155 for (Conversation conversation : account.pendingConferenceLeaves) {
156 leaveMuc(conversation);
157 }
158 for (Conversation conversation : account.pendingConferenceJoins) {
159 joinMuc(conversation);
160 }
161 mJingleConnectionManager.cancelInTransmission();
162 List<Conversation> conversations = getConversations();
163 for (int i = 0; i < conversations.size(); ++i) {
164 if (conversations.get(i).getAccount() == account) {
165 conversations.get(i).startOtrIfNeeded();
166 sendUnsendMessages(conversations.get(i));
167 }
168 }
169 syncDirtyContacts(account);
170 scheduleWakeupCall(PING_MAX_INTERVAL, true);
171 } else if (account.getStatus() == Account.STATUS_OFFLINE) {
172 resetSendingToWaiting(account);
173 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
174 int timeToReconnect = mRandom.nextInt(50) + 10;
175 scheduleWakeupCall(timeToReconnect, false);
176 }
177 } else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) {
178 databaseBackend.updateAccount(account);
179 reconnectAccount(account, true);
180 } else if ((account.getStatus() != Account.STATUS_CONNECTING)
181 && (account.getStatus() != Account.STATUS_NO_INTERNET)) {
182 XmppConnection connection = account.getXmppConnection();
183 if (connection != null) {
184 int next = connection.getTimeToNextAttempt();
185 Log.d(LOGTAG, account.getJid()
186 + ": error connecting account. try again in "
187 + next + "s for the "
188 + (connection.getAttempt() + 1) + " time");
189 scheduleWakeupCall((int) (next * 1.2), false);
190 }
191 }
192 UIHelper.showErrorNotification(getApplicationContext(),
193 getAccounts());
194 }
195 };
196
197 private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
198
199 @Override
200 public void onJinglePacketReceived(Account account, JinglePacket packet) {
201 mJingleConnectionManager.deliverPacket(account, packet);
202 }
203 };
204
205 private OpenPgpServiceConnection pgpServiceConnection;
206 private PgpEngine mPgpEngine = null;
207 private Intent pingIntent;
208 private PendingIntent pendingPingIntent = null;
209 private WakeLock wakeLock;
210 private PowerManager pm;
211 private OnBindListener mOnBindListener = new OnBindListener() {
212
213 @Override
214 public void onBind(final Account account) {
215 account.getRoster().clearPresences();
216 account.clearPresences(); // self presences
217 account.pendingConferenceJoins.clear();
218 account.pendingConferenceLeaves.clear();
219 fetchRosterFromServer(account);
220 fetchBookmarks(account);
221 sendPresencePacket(account,
222 mPresenceGenerator.sendPresence(account));
223 connectMultiModeConversations(account);
224 updateConversationUi();
225 }
226 };
227
228 private OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
229
230 @Override
231 public void onMessageAcknowledged(Account account, String uuid) {
232 for (Conversation conversation : getConversations()) {
233 if (conversation.getAccount() == account) {
234 for (Message message : conversation.getMessages()) {
235 if ((message.getStatus() == Message.STATUS_UNSEND || message
236 .getStatus() == Message.STATUS_WAITING)
237 && message.getUuid().equals(uuid)) {
238 markMessage(message, Message.STATUS_SEND);
239 return;
240 }
241 }
242 }
243 }
244 }
245 };
246
247 public PgpEngine getPgpEngine() {
248 if (pgpServiceConnection.isBound()) {
249 if (this.mPgpEngine == null) {
250 this.mPgpEngine = new PgpEngine(new OpenPgpApi(
251 getApplicationContext(),
252 pgpServiceConnection.getService()), this);
253 }
254 return mPgpEngine;
255 } else {
256 return null;
257 }
258
259 }
260
261 public FileBackend getFileBackend() {
262 return this.fileBackend;
263 }
264
265 public Message attachImageToConversation(final Conversation conversation,
266 final Uri uri, final UiCallback<Message> callback) {
267 final Message message;
268 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
269 message = new Message(conversation, "",
270 Message.ENCRYPTION_DECRYPTED);
271 } else {
272 message = new Message(conversation, "",
273 conversation.getNextEncryption());
274 }
275 message.setPresence(conversation.getNextPresence());
276 message.setType(Message.TYPE_IMAGE);
277 message.setStatus(Message.STATUS_OFFERED);
278 new Thread(new Runnable() {
279
280 @Override
281 public void run() {
282 try {
283 getFileBackend().copyImageToPrivateStorage(message, uri);
284 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
285 getPgpEngine().encrypt(message, callback);
286 } else {
287 callback.success(message);
288 }
289 } catch (FileBackend.ImageCopyException e) {
290 callback.error(e.getResId(), message);
291 }
292 }
293 }).start();
294 return message;
295 }
296
297 public Conversation find(Bookmark bookmark) {
298 return find(bookmark.getAccount(), bookmark.getJid());
299 }
300
301 public Conversation find(Account account, String jid) {
302 return find(getConversations(), account, jid);
303 }
304
305 public class XmppConnectionBinder extends Binder {
306 public XmppConnectionService getService() {
307 return XmppConnectionService.this;
308 }
309 }
310
311 @Override
312 public int onStartCommand(Intent intent, int flags, int startId) {
313 if ((intent != null)
314 && (ACTION_MERGE_PHONE_CONTACTS.equals(intent.getAction()))) {
315 mergePhoneContactsWithRoster();
316 return START_STICKY;
317 } else if ((intent != null)
318 && (Intent.ACTION_SHUTDOWN.equals(intent.getAction()))) {
319 logoutAndSave();
320 return START_NOT_STICKY;
321 }
322 this.wakeLock.acquire();
323 ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
324 .getSystemService(Context.CONNECTIVITY_SERVICE);
325 NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
326 boolean isConnected = activeNetwork != null
327 && activeNetwork.isConnected();
328
329 for (Account account : accounts) {
330 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
331 if (!isConnected) {
332 account.setStatus(Account.STATUS_NO_INTERNET);
333 if (statusListener != null) {
334 statusListener.onStatusChanged(account);
335 }
336 } else {
337 if (account.getStatus() == Account.STATUS_NO_INTERNET) {
338 account.setStatus(Account.STATUS_OFFLINE);
339 if (statusListener != null) {
340 statusListener.onStatusChanged(account);
341 }
342 }
343 if (account.getStatus() == Account.STATUS_ONLINE) {
344 long lastReceived = account.getXmppConnection()
345 .getLastPacketReceived();
346 long lastSent = account.getXmppConnection()
347 .getLastPingSent();
348 if (lastSent - lastReceived >= PING_TIMEOUT * 1000) {
349 Log.d(LOGTAG, account.getJid() + ": ping timeout");
350 this.reconnectAccount(account, true);
351 } else if (SystemClock.elapsedRealtime() - lastReceived >= PING_MIN_INTERVAL * 1000) {
352 account.getXmppConnection().sendPing();
353 this.scheduleWakeupCall(2, false);
354 }
355 } else if (account.getStatus() == Account.STATUS_OFFLINE) {
356 if (account.getXmppConnection() == null) {
357 account.setXmppConnection(this
358 .createConnection(account));
359 }
360 new Thread(account.getXmppConnection()).start();
361 } else if ((account.getStatus() == Account.STATUS_CONNECTING)
362 && ((SystemClock.elapsedRealtime() - account
363 .getXmppConnection().getLastConnect()) / 1000 >= CONNECT_TIMEOUT)) {
364 Log.d(LOGTAG, account.getJid()
365 + ": time out during connect reconnecting");
366 reconnectAccount(account, true);
367 } else {
368 if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
369 reconnectAccount(account, true);
370 }
371 }
372 // in any case. reschedule wakup call
373 this.scheduleWakeupCall(PING_MAX_INTERVAL, true);
374 }
375 if (mOnAccountUpdate != null) {
376 mOnAccountUpdate.onAccountUpdate();
377 }
378 }
379 }
380 if (wakeLock.isHeld()) {
381 try {
382 wakeLock.release();
383 } catch (RuntimeException re) {
384 }
385 }
386 return START_STICKY;
387 }
388
389 @SuppressLint("TrulyRandom")
390 @Override
391 public void onCreate() {
392 ExceptionHelper.init(getApplicationContext());
393 PRNGFixes.apply();
394 this.mRandom = new SecureRandom();
395 this.mMemorizingTrustManager = new MemorizingTrustManager(
396 getApplicationContext());
397 this.databaseBackend = DatabaseBackend
398 .getInstance(getApplicationContext());
399 this.fileBackend = new FileBackend(getApplicationContext());
400 this.accounts = databaseBackend.getAccounts();
401
402 for (Account account : this.accounts) {
403 this.databaseBackend.readRoster(account.getRoster());
404 }
405 this.mergePhoneContactsWithRoster();
406 this.getConversations();
407
408 getContentResolver().registerContentObserver(
409 ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
410 this.pgpServiceConnection = new OpenPgpServiceConnection(
411 getApplicationContext(), "org.sufficientlysecure.keychain");
412 this.pgpServiceConnection.bindToService();
413
414 this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
415 this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
416 "XmppConnectionService");
417 }
418
419 @Override
420 public void onDestroy() {
421 super.onDestroy();
422 this.logoutAndSave();
423 }
424
425 @Override
426 public void onTaskRemoved(Intent rootIntent) {
427 super.onTaskRemoved(rootIntent);
428 this.logoutAndSave();
429 }
430
431 private void logoutAndSave() {
432 for (Account account : accounts) {
433 databaseBackend.writeRoster(account.getRoster());
434 if (account.getXmppConnection() != null) {
435 disconnect(account, false);
436 }
437 }
438 Context context = getApplicationContext();
439 AlarmManager alarmManager = (AlarmManager) context
440 .getSystemService(Context.ALARM_SERVICE);
441 Intent intent = new Intent(context, EventReceiver.class);
442 alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
443 Log.d(LOGTAG, "good bye");
444 stopSelf();
445 }
446
447 protected void scheduleWakeupCall(int seconds, boolean ping) {
448 long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000;
449 Context context = getApplicationContext();
450 AlarmManager alarmManager = (AlarmManager) context
451 .getSystemService(Context.ALARM_SERVICE);
452
453 if (ping) {
454 if (this.pingIntent == null) {
455 this.pingIntent = new Intent(context, EventReceiver.class);
456 this.pingIntent.setAction("ping");
457 this.pingIntent.putExtra("time", timeToWake);
458 this.pendingPingIntent = PendingIntent.getBroadcast(context, 0,
459 this.pingIntent, 0);
460 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
461 timeToWake, pendingPingIntent);
462 } else {
463 long scheduledTime = this.pingIntent.getLongExtra("time", 0);
464 if (scheduledTime < SystemClock.elapsedRealtime()
465 || (scheduledTime > timeToWake)) {
466 this.pingIntent.putExtra("time", timeToWake);
467 alarmManager.cancel(this.pendingPingIntent);
468 this.pendingPingIntent = PendingIntent.getBroadcast(
469 context, 0, this.pingIntent, 0);
470 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
471 timeToWake, pendingPingIntent);
472 }
473 }
474 } else {
475 Intent intent = new Intent(context, EventReceiver.class);
476 intent.setAction("ping_check");
477 PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0,
478 intent, 0);
479 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake,
480 alarmIntent);
481 }
482
483 }
484
485 public XmppConnection createConnection(Account account) {
486 SharedPreferences sharedPref = getPreferences();
487 account.setResource(sharedPref.getString("resource", "mobile")
488 .toLowerCase(Locale.getDefault()));
489 XmppConnection connection = new XmppConnection(account, this);
490 connection.setOnMessagePacketReceivedListener(this.mMessageParser);
491 connection.setOnStatusChangedListener(this.statusListener);
492 connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
493 connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
494 connection.setOnJinglePacketReceivedListener(this.jingleListener);
495 connection.setOnBindListener(this.mOnBindListener);
496 connection
497 .setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
498 return connection;
499 }
500
501 synchronized public void sendMessage(Message message) {
502 Account account = message.getConversation().getAccount();
503 Conversation conv = message.getConversation();
504 MessagePacket packet = null;
505 boolean saveInDb = true;
506 boolean send = false;
507 if (account.getStatus() == Account.STATUS_ONLINE) {
508 if (message.getType() == Message.TYPE_IMAGE) {
509 if (message.getPresence() != null) {
510 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
511 if (!conv.hasValidOtrSession()
512 && (message.getPresence() != null)) {
513 conv.startOtrSession(getApplicationContext(),
514 message.getPresence(), true);
515 message.setStatus(Message.STATUS_WAITING);
516 } else if (conv.hasValidOtrSession()
517 && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
518 mJingleConnectionManager
519 .createNewConnection(message);
520 } else if (message.getPresence() == null) {
521 message.setStatus(Message.STATUS_WAITING);
522 }
523 } else {
524 mJingleConnectionManager.createNewConnection(message);
525 }
526 } else {
527 message.setStatus(Message.STATUS_WAITING);
528 }
529 } else {
530 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
531 if (!conv.hasValidOtrSession()
532 && (message.getPresence() != null)) {
533 conv.startOtrSession(getApplicationContext(),
534 message.getPresence(), true);
535 message.setStatus(Message.STATUS_WAITING);
536 } else if (conv.hasValidOtrSession()
537 && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
538 message.setPresence(conv.getOtrSession().getSessionID()
539 .getUserID());
540 packet = mMessageGenerator.generateOtrChat(message);
541 send = true;
542
543 } else if (message.getPresence() == null) {
544 message.setStatus(Message.STATUS_WAITING);
545 }
546 } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
547 message.getConversation().endOtrIfNeeded();
548 failWaitingOtrMessages(message.getConversation());
549 packet = mMessageGenerator.generatePgpChat(message);
550 send = true;
551 } else {
552 message.getConversation().endOtrIfNeeded();
553 failWaitingOtrMessages(message.getConversation());
554 packet = mMessageGenerator.generateChat(message);
555 send = true;
556 }
557 }
558 } else {
559 message.setStatus(Message.STATUS_WAITING);
560 if (message.getType() == Message.TYPE_TEXT) {
561 if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
562 String pgpBody = message.getEncryptedBody();
563 String decryptedBody = message.getBody();
564 message.setBody(pgpBody);
565 databaseBackend.createMessage(message);
566 saveInDb = false;
567 message.setBody(decryptedBody);
568 } else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
569 if (conv.hasValidOtrSession()) {
570 message.setPresence(conv.getOtrSession().getSessionID()
571 .getUserID());
572 } else if (!conv.hasValidOtrSession()
573 && message.getPresence() != null) {
574 conv.startOtrSession(getApplicationContext(),
575 message.getPresence(), false);
576 }
577 }
578 }
579
580 }
581 if (saveInDb) {
582 databaseBackend.createMessage(message);
583 }
584 conv.getMessages().add(message);
585 if ((send) && (packet != null)) {
586 if (!account.getXmppConnection().getFeatures().sm()
587 && conv.getMode() != Conversation.MODE_MULTI) {
588 message.setStatus(Message.STATUS_SEND);
589 }
590 sendMessagePacket(account, packet);
591 }
592 updateConversationUi();
593 }
594
595 private void sendUnsendMessages(Conversation conversation) {
596 for (int i = 0; i < conversation.getMessages().size(); ++i) {
597 int status = conversation.getMessages().get(i).getStatus();
598 if (status == Message.STATUS_WAITING) {
599 resendMessage(conversation.getMessages().get(i));
600 }
601 }
602 }
603
604 private void resendMessage(Message message) {
605 Account account = message.getConversation().getAccount();
606 MessagePacket packet = null;
607 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
608 Presences presences = message.getConversation().getContact()
609 .getPresences();
610 if (!message.getConversation().hasValidOtrSession()) {
611 if ((message.getPresence() != null)
612 && (presences.has(message.getPresence()))) {
613 message.getConversation().startOtrSession(
614 getApplicationContext(), message.getPresence(),
615 true);
616 } else {
617 if (presences.size() == 1) {
618 String presence = presences.asStringArray()[0];
619 message.getConversation().startOtrSession(
620 getApplicationContext(), presence, true);
621 }
622 }
623 } else {
624 if (message.getConversation().getOtrSession()
625 .getSessionStatus() == SessionStatus.ENCRYPTED) {
626 if (message.getType() == Message.TYPE_TEXT) {
627 packet = mMessageGenerator.generateOtrChat(message,
628 true);
629 } else if (message.getType() == Message.TYPE_IMAGE) {
630 mJingleConnectionManager.createNewConnection(message);
631 }
632 }
633 }
634 } else if (message.getType() == Message.TYPE_TEXT) {
635 if (message.getEncryption() == Message.ENCRYPTION_NONE) {
636 packet = mMessageGenerator.generateChat(message, true);
637 } else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED)
638 || (message.getEncryption() == Message.ENCRYPTION_PGP)) {
639 packet = mMessageGenerator.generatePgpChat(message, true);
640 }
641 } else if (message.getType() == Message.TYPE_IMAGE) {
642 Presences presences = message.getConversation().getContact()
643 .getPresences();
644 if ((message.getPresence() != null)
645 && (presences.has(message.getPresence()))) {
646 markMessage(message, Message.STATUS_OFFERED);
647 mJingleConnectionManager.createNewConnection(message);
648 } else {
649 if (presences.size() == 1) {
650 String presence = presences.asStringArray()[0];
651 message.setPresence(presence);
652 markMessage(message, Message.STATUS_OFFERED);
653 mJingleConnectionManager.createNewConnection(message);
654 }
655 }
656 }
657 if (packet != null) {
658 sendMessagePacket(account, packet);
659 markMessage(message, Message.STATUS_SEND);
660 }
661 }
662
663 public void fetchRosterFromServer(Account account) {
664 IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
665 if (!"".equals(account.getRosterVersion())) {
666 Log.d(LOGTAG, account.getJid() + ": fetching roster version "
667 + account.getRosterVersion());
668 } else {
669 Log.d(LOGTAG, account.getJid() + ": fetching roster");
670 }
671 iqPacket.query("jabber:iq:roster").setAttribute("ver",
672 account.getRosterVersion());
673 account.getXmppConnection().sendIqPacket(iqPacket,
674 new OnIqPacketReceived() {
675
676 @Override
677 public void onIqPacketReceived(final Account account,
678 IqPacket packet) {
679 Element query = packet.findChild("query");
680 if (query != null) {
681 account.getRoster().markAllAsNotInRoster();
682 mIqParser.rosterItems(account, query);
683 }
684 }
685 });
686 }
687
688 public void fetchBookmarks(Account account) {
689 IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
690 Element query = iqPacket.query("jabber:iq:private");
691 query.addChild("storage", "storage:bookmarks");
692 OnIqPacketReceived callback = new OnIqPacketReceived() {
693
694 @Override
695 public void onIqPacketReceived(Account account, IqPacket packet) {
696 Element query = packet.query();
697 List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>();
698 Element storage = query.findChild("storage",
699 "storage:bookmarks");
700 if (storage != null) {
701 for (Element item : storage.getChildren()) {
702 if (item.getName().equals("conference")) {
703 Bookmark bookmark = Bookmark.parse(item, account);
704 bookmarks.add(bookmark);
705 Conversation conversation = find(bookmark);
706 if (conversation != null) {
707 conversation.setBookmark(bookmark);
708 } else {
709 if (bookmark.autojoin()) {
710 conversation = findOrCreateConversation(
711 account, bookmark.getJid(), true);
712 conversation.setBookmark(bookmark);
713 joinMuc(conversation);
714 }
715 }
716 }
717 }
718 }
719 account.setBookmarks(bookmarks);
720 }
721 };
722 sendIqPacket(account, iqPacket, callback);
723
724 }
725
726 public void pushBookmarks(Account account) {
727 IqPacket iqPacket = new IqPacket(IqPacket.TYPE_SET);
728 Element query = iqPacket.query("jabber:iq:private");
729 Element storage = query.addChild("storage", "storage:bookmarks");
730 for (Bookmark bookmark : account.getBookmarks()) {
731 storage.addChild(bookmark.toElement());
732 }
733 sendIqPacket(account, iqPacket, null);
734 }
735
736 private void mergePhoneContactsWithRoster() {
737 PhoneHelper.loadPhoneContacts(getApplicationContext(),
738 new OnPhoneContactsLoadedListener() {
739 @Override
740 public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
741 for (Account account : accounts) {
742 account.getRoster().clearSystemAccounts();
743 }
744 for (Bundle phoneContact : phoneContacts) {
745 for (Account account : accounts) {
746 String jid = phoneContact.getString("jid");
747 Contact contact = account.getRoster()
748 .getContact(jid);
749 String systemAccount = phoneContact
750 .getInt("phoneid")
751 + "#"
752 + phoneContact.getString("lookup");
753 contact.setSystemAccount(systemAccount);
754 contact.setPhotoUri(phoneContact
755 .getString("photouri"));
756 contact.setSystemName(phoneContact
757 .getString("displayname"));
758 }
759 }
760 }
761 });
762 }
763
764 public List<Conversation> getConversations() {
765 if (this.conversations == null) {
766 Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
767 for (Account account : this.accounts) {
768 accountLookupTable.put(account.getUuid(), account);
769 }
770 this.conversations = databaseBackend
771 .getConversations(Conversation.STATUS_AVAILABLE);
772 for (Conversation conv : this.conversations) {
773 Account account = accountLookupTable.get(conv.getAccountUuid());
774 conv.setAccount(account);
775 conv.setMessages(databaseBackend.getMessages(conv, 50));
776 }
777 }
778
779 return this.conversations;
780 }
781
782 public void populateWithOrderedConversations(List<Conversation> list) {
783 list.clear();
784 list.addAll(getConversations());
785 Collections.sort(list, new Comparator<Conversation>() {
786 @Override
787 public int compare(Conversation lhs, Conversation rhs) {
788 Message left = lhs.getLatestMessage();
789 Message right = rhs.getLatestMessage();
790 if (left.getTimeSent() > right.getTimeSent()) {
791 return -1;
792 } else if (left.getTimeSent() < right.getTimeSent()) {
793 return 1;
794 } else {
795 return 0;
796 }
797 }
798 });
799 }
800
801 public List<Message> getMoreMessages(Conversation conversation,
802 long timestamp) {
803 List<Message> messages = databaseBackend.getMessages(conversation, 50,
804 timestamp);
805 for (Message message : messages) {
806 message.setConversation(conversation);
807 }
808 return messages;
809 }
810
811 public List<Account> getAccounts() {
812 return this.accounts;
813 }
814
815 public Conversation find(List<Conversation> haystack, Contact contact) {
816 for (Conversation conversation : haystack) {
817 if (conversation.getContact() == contact) {
818 return conversation;
819 }
820 }
821 return null;
822 }
823
824 public Conversation find(List<Conversation> haystack, Account account,
825 String jid) {
826 for (Conversation conversation : haystack) {
827 if ((conversation.getAccount().equals(account))
828 && (conversation.getContactJid().split("/")[0].equals(jid))) {
829 return conversation;
830 }
831 }
832 return null;
833 }
834
835 public Conversation findOrCreateConversation(Account account, String jid,
836 boolean muc) {
837 Conversation conversation = find(account, jid);
838 if (conversation != null) {
839 return conversation;
840 }
841 conversation = databaseBackend.findConversation(account, jid);
842 if (conversation != null) {
843 conversation.setStatus(Conversation.STATUS_AVAILABLE);
844 conversation.setAccount(account);
845 if (muc) {
846 conversation.setMode(Conversation.MODE_MULTI);
847 } else {
848 conversation.setMode(Conversation.MODE_SINGLE);
849 }
850 conversation.setMessages(databaseBackend.getMessages(conversation,
851 50));
852 this.databaseBackend.updateConversation(conversation);
853 } else {
854 String conversationName;
855 Contact contact = account.getRoster().getContact(jid);
856 if (contact != null) {
857 conversationName = contact.getDisplayName();
858 } else {
859 conversationName = jid.split("@")[0];
860 }
861 if (muc) {
862 conversation = new Conversation(conversationName, account, jid,
863 Conversation.MODE_MULTI);
864 } else {
865 conversation = new Conversation(conversationName, account, jid,
866 Conversation.MODE_SINGLE);
867 }
868 this.databaseBackend.createConversation(conversation);
869 }
870 this.conversations.add(conversation);
871 updateConversationUi();
872 return conversation;
873 }
874
875 public void archiveConversation(Conversation conversation) {
876 if (conversation.getMode() == Conversation.MODE_MULTI) {
877 Bookmark bookmark = conversation.getBookmark();
878 if (bookmark != null && bookmark.autojoin()) {
879 bookmark.setAutojoin(false);
880 pushBookmarks(bookmark.getAccount());
881 }
882 leaveMuc(conversation);
883 } else {
884 conversation.endOtrIfNeeded();
885 }
886 this.databaseBackend.updateConversation(conversation);
887 this.conversations.remove(conversation);
888 updateConversationUi();
889 }
890
891 public void clearConversationHistory(Conversation conversation) {
892 this.databaseBackend.deleteMessagesInConversation(conversation);
893 this.fileBackend.removeFiles(conversation);
894 conversation.getMessages().clear();
895 updateConversationUi();
896 }
897
898 public int getConversationCount() {
899 return this.databaseBackend.getConversationCount();
900 }
901
902 public void createAccount(Account account) {
903 databaseBackend.createAccount(account);
904 this.accounts.add(account);
905 this.reconnectAccount(account, false);
906 updateAccountUi();
907 }
908
909 public void updateAccount(Account account) {
910 this.statusListener.onStatusChanged(account);
911 databaseBackend.updateAccount(account);
912 reconnectAccount(account, false);
913 updateAccountUi();
914 UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
915 }
916
917 public void deleteAccount(Account account) {
918 for (Conversation conversation : conversations) {
919 if (conversation.getAccount() == account) {
920 if (conversation.getMode() == Conversation.MODE_MULTI) {
921 leaveMuc(conversation);
922 } else if (conversation.getMode() == Conversation.MODE_SINGLE) {
923 conversation.endOtrIfNeeded();
924 }
925 conversations.remove(conversation);
926 }
927 }
928 if (account.getXmppConnection() != null) {
929 this.disconnect(account, true);
930 }
931 databaseBackend.deleteAccount(account);
932 this.accounts.remove(account);
933 updateAccountUi();
934 UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
935 }
936
937 public void setOnConversationListChangedListener(
938 OnConversationUpdate listener) {
939 this.mOnConversationUpdate = listener;
940 this.convChangedListenerCount++;
941 }
942
943 public void removeOnConversationListChangedListener() {
944 this.convChangedListenerCount--;
945 if (this.convChangedListenerCount == 0) {
946 this.mOnConversationUpdate = null;
947 }
948 }
949
950 public void setOnAccountListChangedListener(OnAccountUpdate listener) {
951 this.mOnAccountUpdate = listener;
952 this.accountChangedListenerCount++;
953 }
954
955 public void removeOnAccountListChangedListener() {
956 this.accountChangedListenerCount--;
957 if (this.accountChangedListenerCount == 0) {
958 this.mOnAccountUpdate = null;
959 }
960 }
961
962 public void setOnRosterUpdateListener(OnRosterUpdate listener) {
963 this.mOnRosterUpdate = listener;
964 }
965
966 public void removeOnRosterUpdateListener() {
967 this.mOnRosterUpdate = null;
968 }
969
970 public void connectMultiModeConversations(Account account) {
971 List<Conversation> conversations = getConversations();
972 for (int i = 0; i < conversations.size(); i++) {
973 Conversation conversation = conversations.get(i);
974 if ((conversation.getMode() == Conversation.MODE_MULTI)
975 && (conversation.getAccount() == account)) {
976 joinMuc(conversation);
977 }
978 }
979 }
980
981 public void joinMuc(Conversation conversation) {
982 Account account = conversation.getAccount();
983 account.pendingConferenceJoins.remove(conversation);
984 account.pendingConferenceLeaves.remove(conversation);
985 if (account.getStatus() == Account.STATUS_ONLINE) {
986 Log.d(LOGTAG,
987 "joining conversation " + conversation.getContactJid());
988 String nick = conversation.getMucOptions().getProposedNick();
989 conversation.getMucOptions().setJoinNick(nick);
990 PresencePacket packet = new PresencePacket();
991 String joinJid = conversation.getMucOptions().getJoinJid();
992 packet.setAttribute("to", conversation.getMucOptions().getJoinJid());
993 Element x = new Element("x");
994 x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
995 String sig = account.getPgpSignature();
996 if (sig != null) {
997 packet.addChild("status").setContent("online");
998 packet.addChild("x", "jabber:x:signed").setContent(sig);
999 }
1000 if (conversation.getMessages().size() != 0) {
1001 final SimpleDateFormat mDateFormat = new SimpleDateFormat(
1002 "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
1003 mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
1004 Date date = new Date(conversation.getLatestMessage()
1005 .getTimeSent() + 1000);
1006 x.addChild("history").setAttribute("since",
1007 mDateFormat.format(date));
1008 }
1009 packet.addChild(x);
1010 sendPresencePacket(account, packet);
1011 if (!joinJid.equals(conversation.getContactJid())) {
1012 conversation.setContactJid(joinJid);
1013 databaseBackend.updateConversation(conversation);
1014 }
1015 } else {
1016 account.pendingConferenceJoins.add(conversation);
1017 }
1018 }
1019
1020 private OnRenameListener renameListener = null;
1021 private IqGenerator mIqGenerator = new IqGenerator();
1022
1023 public void setOnRenameListener(OnRenameListener listener) {
1024 this.renameListener = listener;
1025 }
1026
1027 public void renameInMuc(final Conversation conversation, final String nick) {
1028 final MucOptions options = conversation.getMucOptions();
1029 options.setJoinNick(nick);
1030 if (options.online()) {
1031 Account account = conversation.getAccount();
1032 options.setOnRenameListener(new OnRenameListener() {
1033
1034 @Override
1035 public void onRename(boolean success) {
1036 if (renameListener != null) {
1037 renameListener.onRename(success);
1038 }
1039 if (success) {
1040 conversation.setContactJid(conversation.getMucOptions()
1041 .getJoinJid());
1042 databaseBackend.updateConversation(conversation);
1043 Bookmark bookmark = conversation.getBookmark();
1044 if (bookmark != null) {
1045 bookmark.setNick(nick);
1046 pushBookmarks(bookmark.getAccount());
1047 }
1048 }
1049 }
1050 });
1051 options.flagAboutToRename();
1052 PresencePacket packet = new PresencePacket();
1053 packet.setAttribute("to", options.getJoinJid());
1054 packet.setAttribute("from", conversation.getAccount().getFullJid());
1055
1056 String sig = account.getPgpSignature();
1057 if (sig != null) {
1058 packet.addChild("status").setContent("online");
1059 packet.addChild("x", "jabber:x:signed").setContent(sig);
1060 }
1061 sendPresencePacket(account, packet);
1062 } else {
1063 conversation.setContactJid(options.getJoinJid());
1064 databaseBackend.updateConversation(conversation);
1065 if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
1066 Bookmark bookmark = conversation.getBookmark();
1067 if (bookmark != null) {
1068 bookmark.setNick(nick);
1069 pushBookmarks(bookmark.getAccount());
1070 }
1071 joinMuc(conversation);
1072 }
1073 }
1074 }
1075
1076 public void leaveMuc(Conversation conversation) {
1077 Account account = conversation.getAccount();
1078 account.pendingConferenceJoins.remove(conversation);
1079 account.pendingConferenceLeaves.remove(conversation);
1080 if (account.getStatus() == Account.STATUS_ONLINE) {
1081 PresencePacket packet = new PresencePacket();
1082 packet.setAttribute("to", conversation.getMucOptions().getJoinJid());
1083 packet.setAttribute("from", conversation.getAccount().getFullJid());
1084 packet.setAttribute("type", "unavailable");
1085 sendPresencePacket(conversation.getAccount(), packet);
1086 conversation.getMucOptions().setOffline();
1087 conversation.deregisterWithBookmark();
1088 Log.d(LOGTAG, conversation.getAccount().getJid() + " leaving muc "
1089 + conversation.getContactJid());
1090 } else {
1091 account.pendingConferenceLeaves.add(conversation);
1092 }
1093 }
1094
1095 public void disconnect(Account account, boolean force) {
1096 if ((account.getStatus() == Account.STATUS_ONLINE)
1097 || (account.getStatus() == Account.STATUS_DISABLED)) {
1098 if (!force) {
1099 List<Conversation> conversations = getConversations();
1100 for (int i = 0; i < conversations.size(); i++) {
1101 Conversation conversation = conversations.get(i);
1102 if (conversation.getAccount() == account) {
1103 if (conversation.getMode() == Conversation.MODE_MULTI) {
1104 leaveMuc(conversation);
1105 } else {
1106 conversation.endOtrIfNeeded();
1107 }
1108 }
1109 }
1110 }
1111 account.getXmppConnection().disconnect(force);
1112 }
1113 }
1114
1115 @Override
1116 public IBinder onBind(Intent intent) {
1117 return mBinder;
1118 }
1119
1120 public void updateMessage(Message message) {
1121 databaseBackend.updateMessage(message);
1122 }
1123
1124 protected void syncDirtyContacts(Account account) {
1125 for (Contact contact : account.getRoster().getContacts()) {
1126 if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
1127 pushContactToServer(contact);
1128 }
1129 if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
1130 Log.d(LOGTAG, "dirty delete");
1131 deleteContactOnServer(contact);
1132 }
1133 }
1134 }
1135
1136 public void createContact(Contact contact) {
1137 SharedPreferences sharedPref = getPreferences();
1138 boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1139 if (autoGrant) {
1140 contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
1141 contact.setOption(Contact.Options.ASKING);
1142 }
1143 pushContactToServer(contact);
1144 }
1145
1146 public void onOtrSessionEstablished(Conversation conversation) {
1147 Account account = conversation.getAccount();
1148 List<Message> messages = conversation.getMessages();
1149 Session otrSession = conversation.getOtrSession();
1150 Log.d(LOGTAG, account.getJid() + " otr session established with "
1151 + conversation.getContactJid() + "/"
1152 + otrSession.getSessionID().getUserID());
1153 for (int i = 0; i < messages.size(); ++i) {
1154 Message msg = messages.get(i);
1155 if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING)
1156 && (msg.getEncryption() == Message.ENCRYPTION_OTR)) {
1157 msg.setPresence(otrSession.getSessionID().getUserID());
1158 if (msg.getType() == Message.TYPE_TEXT) {
1159 MessagePacket outPacket = mMessageGenerator
1160 .generateOtrChat(msg, true);
1161 if (outPacket != null) {
1162 msg.setStatus(Message.STATUS_SEND);
1163 databaseBackend.updateMessage(msg);
1164 sendMessagePacket(account, outPacket);
1165 }
1166 } else if (msg.getType() == Message.TYPE_IMAGE) {
1167 mJingleConnectionManager.createNewConnection(msg);
1168 }
1169 }
1170 }
1171 notifyUi(conversation, false);
1172 }
1173
1174 public boolean renewSymmetricKey(Conversation conversation) {
1175 Account account = conversation.getAccount();
1176 byte[] symmetricKey = new byte[32];
1177 this.mRandom.nextBytes(symmetricKey);
1178 Session otrSession = conversation.getOtrSession();
1179 if (otrSession != null) {
1180 MessagePacket packet = new MessagePacket();
1181 packet.setType(MessagePacket.TYPE_CHAT);
1182 packet.setFrom(account.getFullJid());
1183 packet.addChild("private", "urn:xmpp:carbons:2");
1184 packet.addChild("no-copy", "urn:xmpp:hints");
1185 packet.setTo(otrSession.getSessionID().getAccountID() + "/"
1186 + otrSession.getSessionID().getUserID());
1187 try {
1188 packet.setBody(otrSession
1189 .transformSending(CryptoHelper.FILETRANSFER
1190 + CryptoHelper.bytesToHex(symmetricKey)));
1191 sendMessagePacket(account, packet);
1192 conversation.setSymmetricKey(symmetricKey);
1193 return true;
1194 } catch (OtrException e) {
1195 return false;
1196 }
1197 }
1198 return false;
1199 }
1200
1201 public void pushContactToServer(Contact contact) {
1202 contact.resetOption(Contact.Options.DIRTY_DELETE);
1203 contact.setOption(Contact.Options.DIRTY_PUSH);
1204 Account account = contact.getAccount();
1205 if (account.getStatus() == Account.STATUS_ONLINE) {
1206 boolean ask = contact.getOption(Contact.Options.ASKING);
1207 boolean sendUpdates = contact
1208 .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
1209 && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
1210 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1211 iq.query("jabber:iq:roster").addChild(contact.asElement());
1212 account.getXmppConnection().sendIqPacket(iq, null);
1213 if (sendUpdates) {
1214 sendPresencePacket(account,
1215 mPresenceGenerator.sendPresenceUpdatesTo(contact));
1216 }
1217 if (ask) {
1218 sendPresencePacket(account,
1219 mPresenceGenerator.requestPresenceUpdatesFrom(contact));
1220 }
1221 }
1222 }
1223
1224 public void publishAvatar(Account account, Uri image,
1225 final UiCallback<Avatar> callback) {
1226 final Bitmap.CompressFormat format = Defaults.AVATAR_FORMAT;
1227 final int size = Defaults.AVATAR_SIZE;
1228 final Avatar avatar = getFileBackend()
1229 .getPepAvatar(image, size, format);
1230 if (avatar != null) {
1231 avatar.height = size;
1232 avatar.width = size;
1233 if (format.equals(Bitmap.CompressFormat.WEBP)) {
1234 avatar.type = "image/webp";
1235 } else if (format.equals(Bitmap.CompressFormat.JPEG)) {
1236 avatar.type = "image/jpeg";
1237 } else if (format.equals(Bitmap.CompressFormat.PNG)) {
1238 avatar.type = "image/png";
1239 }
1240 if (!getFileBackend().save(avatar)) {
1241 callback.error(R.string.error_saving_avatar, avatar);
1242 return;
1243 }
1244 IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
1245 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1246
1247 @Override
1248 public void onIqPacketReceived(Account account, IqPacket result) {
1249 if (result.getType() == IqPacket.TYPE_RESULT) {
1250 IqPacket packet = XmppConnectionService.this.mIqGenerator
1251 .publishAvatarMetadata(avatar);
1252 sendIqPacket(account, packet, new OnIqPacketReceived() {
1253
1254 @Override
1255 public void onIqPacketReceived(Account account,
1256 IqPacket result) {
1257 if (result.getType() == IqPacket.TYPE_RESULT) {
1258 if (account.setAvatar(avatar.getFilename())) {
1259 databaseBackend.updateAccount(account);
1260 }
1261 callback.success(avatar);
1262 } else {
1263 callback.error(
1264 R.string.error_publish_avatar_server_reject,
1265 avatar);
1266 }
1267 }
1268 });
1269 } else {
1270 callback.error(
1271 R.string.error_publish_avatar_server_reject,
1272 avatar);
1273 }
1274 }
1275 });
1276 } else {
1277 callback.error(R.string.error_publish_avatar_converting, null);
1278 }
1279 }
1280
1281 public void fetchAvatar(Account account, Avatar avatar) {
1282 fetchAvatar(account, avatar, null);
1283 }
1284
1285 public void fetchAvatar(Account account, final Avatar avatar,
1286 final UiCallback<Avatar> callback) {
1287 Log.d(LOGTAG, account.getJid() + ": retrieving avatar for "
1288 + avatar.owner);
1289 IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar);
1290 sendIqPacket(account, packet, new OnIqPacketReceived() {
1291
1292 @Override
1293 public void onIqPacketReceived(Account account, IqPacket result) {
1294 avatar.image = mIqParser.avatarData(result);
1295 if (avatar.image != null) {
1296 if (getFileBackend().save(avatar)) {
1297 if (account.getJid().equals(avatar.owner)) {
1298 if (account.setAvatar(avatar.getFilename())) {
1299 databaseBackend.updateAccount(account);
1300 }
1301 } else {
1302 Contact contact = account.getRoster().getContact(
1303 avatar.owner);
1304 contact.setAvatar(avatar.getFilename());
1305 }
1306 if (callback != null) {
1307 callback.success(avatar);
1308 }
1309 return;
1310 }
1311 }
1312 if (callback != null) {
1313 callback.error(0, null);
1314 }
1315 }
1316 });
1317 }
1318
1319 public void checkForAvatar(Account account,
1320 final UiCallback<Avatar> callback) {
1321 IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
1322 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1323
1324 @Override
1325 public void onIqPacketReceived(Account account, IqPacket packet) {
1326 if (packet.getType() == IqPacket.TYPE_RESULT) {
1327 Element pubsub = packet.findChild("pubsub",
1328 "http://jabber.org/protocol/pubsub");
1329 if (pubsub != null) {
1330 Element items = pubsub.findChild("items");
1331 if (items != null) {
1332 Avatar avatar = Avatar.parseMetadata(items);
1333 if (avatar != null) {
1334 avatar.owner = account.getJid();
1335 if (fileBackend.isAvatarCached(avatar)) {
1336 if (account.setAvatar(avatar.getFilename())) {
1337 databaseBackend.updateAccount(account);
1338 }
1339 callback.success(avatar);
1340 } else {
1341 fetchAvatar(account, avatar, callback);
1342 }
1343 return;
1344 }
1345 }
1346 }
1347 }
1348 callback.error(0, null);
1349 }
1350 });
1351 }
1352
1353 public void deleteContactOnServer(Contact contact) {
1354 contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
1355 contact.resetOption(Contact.Options.DIRTY_PUSH);
1356 contact.setOption(Contact.Options.DIRTY_DELETE);
1357 Account account = contact.getAccount();
1358 if (account.getStatus() == Account.STATUS_ONLINE) {
1359 IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1360 Element item = iq.query("jabber:iq:roster").addChild("item");
1361 item.setAttribute("jid", contact.getJid());
1362 item.setAttribute("subscription", "remove");
1363 account.getXmppConnection().sendIqPacket(iq, null);
1364 }
1365 }
1366
1367 public void updateConversation(Conversation conversation) {
1368 this.databaseBackend.updateConversation(conversation);
1369 }
1370
1371 public void reconnectAccount(final Account account, final boolean force) {
1372 new Thread(new Runnable() {
1373
1374 @Override
1375 public void run() {
1376 if (account.getXmppConnection() != null) {
1377 disconnect(account, force);
1378 }
1379 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1380 if (account.getXmppConnection() == null) {
1381 account.setXmppConnection(createConnection(account));
1382 }
1383 Thread thread = new Thread(account.getXmppConnection());
1384 thread.start();
1385 scheduleWakeupCall((int) (CONNECT_TIMEOUT * 1.2), false);
1386 }
1387 }
1388 }).start();
1389 }
1390
1391 public void invite(Conversation conversation, String contact) {
1392 MessagePacket packet = mMessageGenerator.invite(conversation, contact);
1393 sendMessagePacket(conversation.getAccount(), packet);
1394 }
1395
1396 public void resetSendingToWaiting(Account account) {
1397 for(Conversation conversation : getConversations()) {
1398 if (conversation.getAccount() == account) {
1399 for(Message message : conversation.getMessages()) {
1400 if (message.getStatus() == Message.STATUS_UNSEND) {
1401 markMessage(message, Message.STATUS_WAITING);
1402 }
1403 }
1404 }
1405 }
1406 }
1407
1408 public boolean markMessage(Account account, String recipient, String uuid,
1409 int status) {
1410 for (Conversation conversation : getConversations()) {
1411 if (conversation.getContactJid().equals(recipient)
1412 && conversation.getAccount().equals(account)) {
1413 return markMessage(conversation, uuid, status);
1414 }
1415 }
1416 return false;
1417 }
1418
1419 public boolean markMessage(Conversation conversation, String uuid,
1420 int status) {
1421 for (Message message : conversation.getMessages()) {
1422 if (message.getUuid().equals(uuid)) {
1423 markMessage(message, status);
1424 return true;
1425 }
1426 }
1427 return false;
1428 }
1429
1430 public void markMessage(Message message, int status) {
1431 message.setStatus(status);
1432 databaseBackend.updateMessage(message);
1433 updateConversationUi();
1434 }
1435
1436 public SharedPreferences getPreferences() {
1437 return PreferenceManager
1438 .getDefaultSharedPreferences(getApplicationContext());
1439 }
1440
1441 public boolean confirmMessages() {
1442 return getPreferences().getBoolean("confirm_messages", true);
1443 }
1444
1445 public void notifyUi(Conversation conversation, boolean notify) {
1446 if (mOnConversationUpdate != null) {
1447 mOnConversationUpdate.onConversationUpdate();
1448 } else {
1449 UIHelper.updateNotification(getApplicationContext(),
1450 getConversations(), conversation, notify);
1451 }
1452 }
1453
1454 public void updateConversationUi() {
1455 if (mOnConversationUpdate != null) {
1456 mOnConversationUpdate.onConversationUpdate();
1457 }
1458 }
1459
1460 public void updateAccountUi() {
1461 if (mOnAccountUpdate != null) {
1462 mOnAccountUpdate.onAccountUpdate();
1463 }
1464 }
1465
1466 public void updateRosterUi() {
1467 if (mOnRosterUpdate != null) {
1468 mOnRosterUpdate.onRosterUpdate();
1469 }
1470 }
1471
1472 public Account findAccountByJid(String accountJid) {
1473 for (Account account : this.accounts) {
1474 if (account.getJid().equals(accountJid)) {
1475 return account;
1476 }
1477 }
1478 return null;
1479 }
1480
1481 public Conversation findConversationByUuid(String uuid) {
1482 for (Conversation conversation : getConversations()) {
1483 if (conversation.getUuid().equals(uuid)) {
1484 return conversation;
1485 }
1486 }
1487 return null;
1488 }
1489
1490 public void markRead(Conversation conversation) {
1491 conversation.markRead();
1492 String id = conversation.popLatestMarkableMessageId();
1493 if (confirmMessages() && id != null) {
1494 Account account = conversation.getAccount();
1495 String to = conversation.getContactJid();
1496 this.sendMessagePacket(conversation.getAccount(),
1497 mMessageGenerator.confirm(account, to, id));
1498 }
1499 }
1500
1501 public void failWaitingOtrMessages(Conversation conversation) {
1502 for (Message message : conversation.getMessages()) {
1503 if (message.getEncryption() == Message.ENCRYPTION_OTR
1504 && message.getStatus() == Message.STATUS_WAITING) {
1505 markMessage(message, Message.STATUS_SEND_FAILED);
1506 }
1507 }
1508 }
1509
1510 public SecureRandom getRNG() {
1511 return this.mRandom;
1512 }
1513
1514 public MemorizingTrustManager getMemorizingTrustManager() {
1515 return this.mMemorizingTrustManager;
1516 }
1517
1518 public PowerManager getPowerManager() {
1519 return this.pm;
1520 }
1521
1522 public void replyWithNotAcceptable(Account account, MessagePacket packet) {
1523 if (account.getStatus() == Account.STATUS_ONLINE) {
1524 MessagePacket error = this.mMessageGenerator
1525 .generateNotAcceptable(packet);
1526 sendMessagePacket(account, error);
1527 }
1528 }
1529
1530 public void syncRosterToDisk(final Account account) {
1531 new Thread(new Runnable() {
1532
1533 @Override
1534 public void run() {
1535 databaseBackend.writeRoster(account.getRoster());
1536 }
1537 }).start();
1538
1539 }
1540
1541 public List<String> getKnownHosts() {
1542 List<String> hosts = new ArrayList<String>();
1543 for (Account account : getAccounts()) {
1544 if (!hosts.contains(account.getServer())) {
1545 hosts.add(account.getServer());
1546 }
1547 for (Contact contact : account.getRoster().getContacts()) {
1548 if (contact.showInRoster()) {
1549 String server = contact.getServer();
1550 if (server != null && !hosts.contains(server)) {
1551 hosts.add(server);
1552 }
1553 }
1554 }
1555 }
1556 return hosts;
1557 }
1558
1559 public List<String> getKnownConferenceHosts() {
1560 ArrayList<String> mucServers = new ArrayList<String>();
1561 for (Account account : accounts) {
1562 if (account.getXmppConnection() != null) {
1563 String server = account.getXmppConnection().getMucServer();
1564 if (server != null && !mucServers.contains(server)) {
1565 mucServers.add(server);
1566 }
1567 }
1568 }
1569 return mucServers;
1570 }
1571
1572 public void sendMessagePacket(Account account, MessagePacket packet) {
1573 account.getXmppConnection().sendMessagePacket(packet);
1574 }
1575
1576 public void sendPresencePacket(Account account, PresencePacket packet) {
1577 account.getXmppConnection().sendPresencePacket(packet);
1578 }
1579
1580 public void sendIqPacket(Account account, IqPacket packet,
1581 OnIqPacketReceived callback) {
1582 account.getXmppConnection().sendIqPacket(packet, callback);
1583 }
1584
1585 public MessageGenerator getMessageGenerator() {
1586 return this.mMessageGenerator;
1587 }
1588
1589 public PresenceGenerator getPresenceGenerator() {
1590 return this.mPresenceGenerator;
1591 }
1592
1593 public IqGenerator getIqGenerator() {
1594 return this.mIqGenerator;
1595 }
1596
1597 public JingleConnectionManager getJingleConnectionManager() {
1598 return this.mJingleConnectionManager;
1599 }
1600
1601 public interface OnConversationUpdate {
1602 public void onConversationUpdate();
1603 }
1604
1605 public interface OnAccountUpdate {
1606 public void onAccountUpdate();
1607 }
1608
1609 public interface OnRosterUpdate {
1610 public void onRosterUpdate();
1611 }
1612}