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