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