1package eu.siacs.conversations.services;
2
3import android.annotation.SuppressLint;
4import android.app.AlarmManager;
5import android.app.PendingIntent;
6import android.app.Service;
7import android.content.Context;
8import android.content.Intent;
9import android.content.SharedPreferences;
10import android.database.ContentObserver;
11import android.graphics.Bitmap;
12import android.net.ConnectivityManager;
13import android.net.NetworkInfo;
14import android.net.Uri;
15import android.os.Binder;
16import android.os.Bundle;
17import android.os.FileObserver;
18import android.os.IBinder;
19import android.os.Looper;
20import android.os.PowerManager;
21import android.os.PowerManager.WakeLock;
22import android.os.SystemClock;
23import android.preference.PreferenceManager;
24import android.provider.ContactsContract;
25import android.util.Log;
26import android.util.LruCache;
27
28import net.java.otr4j.OtrException;
29import net.java.otr4j.session.Session;
30import net.java.otr4j.session.SessionID;
31import net.java.otr4j.session.SessionStatus;
32
33import org.openintents.openpgp.util.OpenPgpApi;
34import org.openintents.openpgp.util.OpenPgpServiceConnection;
35
36import java.math.BigInteger;
37import java.security.SecureRandom;
38import java.util.ArrayList;
39import java.util.Collection;
40import java.util.Collections;
41import java.util.Comparator;
42import java.util.Hashtable;
43import java.util.List;
44import java.util.Locale;
45import java.util.Map;
46import java.util.concurrent.CopyOnWriteArrayList;
47
48import de.duenndns.ssl.MemorizingTrustManager;
49import eu.siacs.conversations.Config;
50import eu.siacs.conversations.R;
51import eu.siacs.conversations.crypto.PgpEngine;
52import eu.siacs.conversations.entities.Account;
53import eu.siacs.conversations.entities.Blockable;
54import eu.siacs.conversations.entities.Bookmark;
55import eu.siacs.conversations.entities.Contact;
56import eu.siacs.conversations.entities.Conversation;
57import eu.siacs.conversations.entities.Downloadable;
58import eu.siacs.conversations.entities.DownloadablePlaceholder;
59import eu.siacs.conversations.entities.Message;
60import eu.siacs.conversations.entities.MucOptions;
61import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
62import eu.siacs.conversations.entities.Presences;
63import eu.siacs.conversations.generator.IqGenerator;
64import eu.siacs.conversations.generator.MessageGenerator;
65import eu.siacs.conversations.generator.PresenceGenerator;
66import eu.siacs.conversations.http.HttpConnectionManager;
67import eu.siacs.conversations.parser.IqParser;
68import eu.siacs.conversations.parser.MessageParser;
69import eu.siacs.conversations.parser.PresenceParser;
70import eu.siacs.conversations.persistance.DatabaseBackend;
71import eu.siacs.conversations.persistance.FileBackend;
72import eu.siacs.conversations.ui.UiCallback;
73import eu.siacs.conversations.utils.CryptoHelper;
74import eu.siacs.conversations.utils.ExceptionHelper;
75import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
76import eu.siacs.conversations.utils.PRNGFixes;
77import eu.siacs.conversations.utils.PhoneHelper;
78import eu.siacs.conversations.utils.Xmlns;
79import eu.siacs.conversations.xml.Element;
80import eu.siacs.conversations.xmpp.OnBindListener;
81import eu.siacs.conversations.xmpp.OnContactStatusChanged;
82import eu.siacs.conversations.xmpp.OnIqPacketReceived;
83import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
84import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
85import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
86import eu.siacs.conversations.xmpp.OnStatusChanged;
87import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
88import eu.siacs.conversations.xmpp.XmppConnection;
89import eu.siacs.conversations.xmpp.forms.Data;
90import eu.siacs.conversations.xmpp.forms.Field;
91import eu.siacs.conversations.xmpp.jid.InvalidJidException;
92import eu.siacs.conversations.xmpp.jid.Jid;
93import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
94import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
95import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
96import eu.siacs.conversations.xmpp.pep.Avatar;
97import eu.siacs.conversations.xmpp.stanzas.IqPacket;
98import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
99import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
100
101public class XmppConnectionService extends Service implements OnPhoneContactsLoadedListener {
102
103 public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification";
104 public static final String ACTION_DISABLE_FOREGROUND = "disable_foreground";
105 private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
106 public static final String ACTION_TRY_AGAIN = "try_again";
107 public static final String ACTION_DISABLE_ACCOUNT = "disable_account";
108 private ContentObserver contactObserver = new ContentObserver(null) {
109 @Override
110 public void onChange(boolean selfChange) {
111 super.onChange(selfChange);
112 Intent intent = new Intent(getApplicationContext(),
113 XmppConnectionService.class);
114 intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
115 startService(intent);
116 }
117 };
118 private final IBinder mBinder = new XmppConnectionBinder();
119 private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
120 private final FileObserver fileObserver = new FileObserver(
121 FileBackend.getConversationsImageDirectory()) {
122
123 @Override
124 public void onEvent(int event, String path) {
125 if (event == FileObserver.DELETE) {
126 markFileDeleted(path.split("\\.")[0]);
127 }
128 }
129 };
130 private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
131
132 @Override
133 public void onJinglePacketReceived(Account account, JinglePacket packet) {
134 mJingleConnectionManager.deliverPacket(account, packet);
135 }
136 };
137 private final OnBindListener mOnBindListener = new OnBindListener() {
138
139 @Override
140 public void onBind(final Account account) {
141 account.getRoster().clearPresences();
142 account.pendingConferenceJoins.clear();
143 account.pendingConferenceLeaves.clear();
144 fetchRosterFromServer(account);
145 fetchBookmarks(account);
146 sendPresence(account);
147 connectMultiModeConversations(account);
148 updateConversationUi();
149 }
150 };
151 private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
152
153 @Override
154 public void onMessageAcknowledged(Account account, String uuid) {
155 for (final Conversation conversation : getConversations()) {
156 if (conversation.getAccount() == account) {
157 Message message = conversation.findUnsentMessageWithUuid(uuid);
158 if (message != null) {
159 markMessage(message, Message.STATUS_SEND);
160 if (conversation.setLastMessageTransmitted(System.currentTimeMillis())) {
161 databaseBackend.updateConversation(conversation);
162 }
163 }
164 }
165 }
166 }
167 };
168 private final IqGenerator mIqGenerator = new IqGenerator(this);
169 public DatabaseBackend databaseBackend;
170 public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
171
172 @Override
173 public void onContactStatusChanged(Contact contact, boolean online) {
174 Conversation conversation = find(getConversations(), contact);
175 if (conversation != null) {
176 if (online && contact.getPresences().size() > 1) {
177 conversation.endOtrIfNeeded();
178 } else {
179 conversation.resetOtrSession();
180 }
181 if (online && (contact.getPresences().size() == 1)) {
182 sendUnsentMessages(conversation);
183 }
184 }
185 }
186 };
187 private FileBackend fileBackend = new FileBackend(this);
188 private MemorizingTrustManager mMemorizingTrustManager;
189 private NotificationService mNotificationService = new NotificationService(
190 this);
191 private OnMessagePacketReceived mMessageParser = new MessageParser(this);
192 private OnPresencePacketReceived mPresenceParser = new PresenceParser(this);
193 private IqParser mIqParser = new IqParser(this);
194 private MessageGenerator mMessageGenerator = new MessageGenerator(this);
195 private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
196 private List<Account> accounts;
197 private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
198 this);
199 private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
200 this);
201 private AvatarService mAvatarService = new AvatarService(this);
202 private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
203 private OnConversationUpdate mOnConversationUpdate = null;
204 private Integer convChangedListenerCount = 0;
205 private OnAccountUpdate mOnAccountUpdate = null;
206 private OnStatusChanged statusListener = new OnStatusChanged() {
207
208 @Override
209 public void onStatusChanged(Account account) {
210 XmppConnection connection = account.getXmppConnection();
211 if (mOnAccountUpdate != null) {
212 mOnAccountUpdate.onAccountUpdate();
213 }
214 if (account.getStatus() == Account.State.ONLINE) {
215 for (Conversation conversation : account.pendingConferenceLeaves) {
216 leaveMuc(conversation);
217 }
218 for (Conversation conversation : account.pendingConferenceJoins) {
219 joinMuc(conversation);
220 }
221 mMessageArchiveService.executePendingQueries(account);
222 mJingleConnectionManager.cancelInTransmission();
223 List<Conversation> conversations = getConversations();
224 for (Conversation conversation : conversations) {
225 if (conversation.getAccount() == account) {
226 conversation.startOtrIfNeeded();
227 sendUnsentMessages(conversation);
228 }
229 }
230 if (connection != null && connection.getFeatures().csi()) {
231 if (checkListeners()) {
232 Log.d(Config.LOGTAG, account.getJid().toBareJid()
233 + " sending csi//inactive");
234 connection.sendInactive();
235 } else {
236 Log.d(Config.LOGTAG, account.getJid().toBareJid()
237 + " sending csi//active");
238 connection.sendActive();
239 }
240 }
241 syncDirtyContacts(account);
242 scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode());
243 } else if (account.getStatus() == Account.State.OFFLINE) {
244 resetSendingToWaiting(account);
245 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
246 int timeToReconnect = mRandom.nextInt(50) + 10;
247 scheduleWakeUpCall(timeToReconnect,account.getUuid().hashCode());
248 }
249 } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
250 databaseBackend.updateAccount(account);
251 reconnectAccount(account, true);
252 } else if ((account.getStatus() != Account.State.CONNECTING)
253 && (account.getStatus() != Account.State.NO_INTERNET)) {
254 if (connection != null) {
255 int next = connection.getTimeToNextAttempt();
256 Log.d(Config.LOGTAG, account.getJid().toBareJid()
257 + ": error connecting account. try again in "
258 + next + "s for the "
259 + (connection.getAttempt() + 1) + " time");
260 scheduleWakeUpCall(next,account.getUuid().hashCode());
261 }
262 }
263 getNotificationService().updateErrorNotification();
264 }
265 };
266 private int accountChangedListenerCount = 0;
267 private OnRosterUpdate mOnRosterUpdate = null;
268 private OnUpdateBlocklist mOnUpdateBlocklist = null;
269 private int updateBlocklistListenerCount = 0;
270 private int rosterChangedListenerCount = 0;
271 private OnMucRosterUpdate mOnMucRosterUpdate = null;
272 private int mucRosterChangedListenerCount = 0;
273 private SecureRandom mRandom;
274 private OpenPgpServiceConnection pgpServiceConnection;
275 private PgpEngine mPgpEngine = null;
276 private WakeLock wakeLock;
277 private PowerManager pm;
278 private LruCache<String, Bitmap> mBitmapCache;
279 private Thread mPhoneContactMergerThread;
280
281 private boolean mRestoredFromDatabase = false;
282 public boolean areMessagesInitialized() {
283 return this.mRestoredFromDatabase;
284 }
285
286 public PgpEngine getPgpEngine() {
287 if (pgpServiceConnection.isBound()) {
288 if (this.mPgpEngine == null) {
289 this.mPgpEngine = new PgpEngine(new OpenPgpApi(
290 getApplicationContext(),
291 pgpServiceConnection.getService()), this);
292 }
293 return mPgpEngine;
294 } else {
295 return null;
296 }
297
298 }
299
300 public FileBackend getFileBackend() {
301 return this.fileBackend;
302 }
303
304 public AvatarService getAvatarService() {
305 return this.mAvatarService;
306 }
307
308 public void attachFileToConversation(final Conversation conversation,
309 final Uri uri,
310 final UiCallback<Message> callback) {
311 final Message message;
312 if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
313 message = new Message(conversation, "",
314 Message.ENCRYPTION_DECRYPTED);
315 } else {
316 message = new Message(conversation, "",
317 conversation.getNextEncryption(forceEncryption()));
318 }
319 message.setCounterpart(conversation.getNextCounterpart());
320 message.setType(Message.TYPE_FILE);
321 message.setStatus(Message.STATUS_OFFERED);
322 String path = getFileBackend().getOriginalPath(uri);
323 if (path!=null) {
324 message.setRelativeFilePath(path);
325 getFileBackend().updateFileParams(message);
326 if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
327 getPgpEngine().encrypt(message, callback);
328 } else {
329 callback.success(message);
330 }
331 } else {
332 new Thread(new Runnable() {
333 @Override
334 public void run() {
335 try {
336 getFileBackend().copyFileToPrivateStorage(message, uri);
337 getFileBackend().updateFileParams(message);
338 if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
339 getPgpEngine().encrypt(message, callback);
340 } else {
341 callback.success(message);
342 }
343 } catch (FileBackend.FileCopyException e) {
344 callback.error(e.getResId(),message);
345 }
346 }
347 }).start();
348
349 }
350 }
351
352 public void attachImageToConversation(final Conversation conversation,
353 final Uri uri, final UiCallback<Message> callback) {
354 final Message message;
355 if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
356 message = new Message(conversation, "",
357 Message.ENCRYPTION_DECRYPTED);
358 } else {
359 message = new Message(conversation, "",
360 conversation.getNextEncryption(forceEncryption()));
361 }
362 message.setCounterpart(conversation.getNextCounterpart());
363 message.setType(Message.TYPE_IMAGE);
364 message.setStatus(Message.STATUS_OFFERED);
365 new Thread(new Runnable() {
366
367 @Override
368 public void run() {
369 try {
370 getFileBackend().copyImageToPrivateStorage(message, uri);
371 if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
372 getPgpEngine().encrypt(message, callback);
373 } else {
374 callback.success(message);
375 }
376 } catch (final FileBackend.FileCopyException e) {
377 callback.error(e.getResId(), message);
378 }
379 }
380 }).start();
381 }
382
383 public Conversation find(Bookmark bookmark) {
384 return find(bookmark.getAccount(), bookmark.getJid());
385 }
386
387 public Conversation find(final Account account, final Jid jid) {
388 return find(getConversations(), account, jid);
389 }
390
391 @Override
392 public int onStartCommand(Intent intent, int flags, int startId) {
393 final String action = intent == null ? null : intent.getAction();
394 if (action != null) {
395 switch (action) {
396 case ACTION_MERGE_PHONE_CONTACTS:
397 if (mRestoredFromDatabase) {
398 PhoneHelper.loadPhoneContacts(getApplicationContext(),
399 new CopyOnWriteArrayList<Bundle>(),
400 this);
401 }
402 return START_STICKY;
403 case Intent.ACTION_SHUTDOWN:
404 logoutAndSave();
405 return START_NOT_STICKY;
406 case ACTION_CLEAR_NOTIFICATION:
407 mNotificationService.clear();
408 break;
409 case ACTION_DISABLE_FOREGROUND:
410 getPreferences().edit().putBoolean("keep_foreground_service",false).commit();
411 toggleForegroundService();
412 break;
413 case ACTION_TRY_AGAIN:
414 for(Account account : accounts) {
415 if (account.hasErrorStatus()) {
416 final XmppConnection connection = account.getXmppConnection();
417 if (connection != null) {
418 connection.resetAttemptCount();
419 }
420 }
421 }
422 break;
423 case ACTION_DISABLE_ACCOUNT:
424 try {
425 String jid = intent.getStringExtra("account");
426 Account account = jid == null ? null : findAccountByJid(Jid.fromString(jid));
427 if (account != null) {
428 account.setOption(Account.OPTION_DISABLED,true);
429 updateAccount(account);
430 }
431 } catch (final InvalidJidException ignored) {
432 break;
433 }
434 break;
435 }
436 }
437 this.wakeLock.acquire();
438
439 for (Account account : accounts) {
440 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
441 if (!hasInternetConnection()) {
442 account.setStatus(Account.State.NO_INTERNET);
443 if (statusListener != null) {
444 statusListener.onStatusChanged(account);
445 }
446 } else {
447 if (account.getStatus() == Account.State.NO_INTERNET) {
448 account.setStatus(Account.State.OFFLINE);
449 if (statusListener != null) {
450 statusListener.onStatusChanged(account);
451 }
452 }
453 if (account.getStatus() == Account.State.ONLINE) {
454 long lastReceived = account.getXmppConnection().getLastPacketReceived();
455 long lastSent = account.getXmppConnection().getLastPingSent();
456 long pingInterval = "ui".equals(action) ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
457 long msToNextPing = (Math.max(lastReceived,lastSent) + pingInterval) - SystemClock.elapsedRealtime();
458 if (lastSent > lastReceived && (lastSent + Config.PING_TIMEOUT * 1000) < SystemClock.elapsedRealtime()) {
459 Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": ping timeout");
460 this.reconnectAccount(account, true);
461 } else if (msToNextPing <= 0) {
462 account.getXmppConnection().sendPing();
463 Log.d(Config.LOGTAG, account.getJid().toBareJid()+" send ping");
464 this.scheduleWakeUpCall(Config.PING_TIMEOUT,account.getUuid().hashCode());
465 } else {
466 this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
467 }
468 } else if (account.getStatus() == Account.State.OFFLINE) {
469 if (account.getXmppConnection() == null) {
470 account.setXmppConnection(this.createConnection(account));
471 }
472 new Thread(account.getXmppConnection()).start();
473 } else if (account.getStatus() == Account.State.CONNECTING) {
474 long timeout = Config.CONNECT_TIMEOUT - ((SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000);
475 if (timeout < 0) {
476 Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting");
477 reconnectAccount(account, true);
478 } else {
479 scheduleWakeUpCall((int) timeout,account.getUuid().hashCode());
480 }
481 } else {
482 if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
483 reconnectAccount(account, true);
484 }
485 }
486
487 }
488 if (mOnAccountUpdate != null) {
489 mOnAccountUpdate.onAccountUpdate();
490 }
491 }
492 }
493 /*PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE);
494 if (!pm.isScreenOn()) {
495 removeStaleListeners();
496 }*/
497 if (wakeLock.isHeld()) {
498 try {
499 wakeLock.release();
500 } catch (final RuntimeException ignored) {
501 }
502 }
503 return START_STICKY;
504 }
505
506 public boolean hasInternetConnection() {
507 ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
508 .getSystemService(Context.CONNECTIVITY_SERVICE);
509 NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
510 return activeNetwork != null && activeNetwork.isConnected();
511 }
512
513 @SuppressLint("TrulyRandom")
514 @Override
515 public void onCreate() {
516 ExceptionHelper.init(getApplicationContext());
517 PRNGFixes.apply();
518 this.mRandom = new SecureRandom();
519 this.mMemorizingTrustManager = new MemorizingTrustManager(
520 getApplicationContext());
521
522 final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
523 final int cacheSize = maxMemory / 8;
524 this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
525 @Override
526 protected int sizeOf(final String key, final Bitmap bitmap) {
527 return bitmap.getByteCount() / 1024;
528 }
529 };
530
531 this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
532 this.accounts = databaseBackend.getAccounts();
533
534 for (final Account account : this.accounts) {
535 account.initOtrEngine(this);
536 }
537 restoreFromDatabase();
538
539 getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
540 this.fileObserver.startWatching();
541 this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain");
542 this.pgpServiceConnection.bindToService();
543
544 this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
545 this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"XmppConnectionService");
546 toggleForegroundService();
547 }
548
549 public void toggleForegroundService() {
550 if (getPreferences().getBoolean("keep_foreground_service",false)) {
551 startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
552 } else {
553 stopForeground(true);
554 }
555 }
556
557 @Override
558 public void onTaskRemoved(final Intent rootIntent) {
559 super.onTaskRemoved(rootIntent);
560 if (!getPreferences().getBoolean("keep_foreground_service",false)) {
561 this.logoutAndSave();
562 }
563 }
564
565 private void logoutAndSave() {
566 for (final Account account : accounts) {
567 databaseBackend.writeRoster(account.getRoster());
568 if (account.getXmppConnection() != null) {
569 disconnect(account, false);
570 }
571 }
572 Context context = getApplicationContext();
573 AlarmManager alarmManager = (AlarmManager) context
574 .getSystemService(Context.ALARM_SERVICE);
575 Intent intent = new Intent(context, EventReceiver.class);
576 alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
577 Log.d(Config.LOGTAG, "good bye");
578 stopSelf();
579 }
580
581 protected void scheduleWakeUpCall(int seconds, int requestCode) {
582 final long timeToWake = SystemClock.elapsedRealtime() + (seconds + 1) * 1000;
583
584 Context context = getApplicationContext();
585 AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
586
587 Intent intent = new Intent(context, EventReceiver.class);
588 intent.setAction("ping");
589 PendingIntent alarmIntent = PendingIntent.getBroadcast(context, requestCode, intent, 0);
590 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, alarmIntent);
591 }
592
593 public XmppConnection createConnection(final Account account) {
594 final SharedPreferences sharedPref = getPreferences();
595 account.setResource(sharedPref.getString("resource", "mobile")
596 .toLowerCase(Locale.getDefault()));
597 final XmppConnection connection = new XmppConnection(account, this);
598 connection.setOnMessagePacketReceivedListener(this.mMessageParser);
599 connection.setOnStatusChangedListener(this.statusListener);
600 connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
601 connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
602 connection.setOnJinglePacketReceivedListener(this.jingleListener);
603 connection.setOnBindListener(this.mOnBindListener);
604 connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
605 connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
606 return connection;
607 }
608
609 public void sendMessage(final Message message) {
610 final Account account = message.getConversation().getAccount();
611 account.deactivateGracePeriod();
612 final Conversation conv = message.getConversation();
613 MessagePacket packet = null;
614 boolean saveInDb = true;
615 boolean send = false;
616 if (account.getStatus() == Account.State.ONLINE
617 && account.getXmppConnection() != null) {
618 if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
619 if (message.getCounterpart() != null) {
620 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
621 if (!conv.hasValidOtrSession()) {
622 conv.startOtrSession(message.getCounterpart().getResourcepart(),true);
623 message.setStatus(Message.STATUS_WAITING);
624 } else if (conv.hasValidOtrSession()
625 && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
626 mJingleConnectionManager
627 .createNewConnection(message);
628 }
629 } else {
630 mJingleConnectionManager.createNewConnection(message);
631 }
632 } else {
633 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
634 conv.startOtrIfNeeded();
635 }
636 message.setStatus(Message.STATUS_WAITING);
637 }
638 } else {
639 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
640 if (!conv.hasValidOtrSession() && (message.getCounterpart() != null)) {
641 conv.startOtrSession(message.getCounterpart().getResourcepart(), true);
642 message.setStatus(Message.STATUS_WAITING);
643 } else if (conv.hasValidOtrSession()) {
644 if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
645 packet = mMessageGenerator.generateOtrChat(message);
646 send = true;
647 } else {
648 message.setStatus(Message.STATUS_WAITING);
649 conv.startOtrIfNeeded();
650 }
651 } else {
652 message.setStatus(Message.STATUS_WAITING);
653 }
654 } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
655 message.getConversation().endOtrIfNeeded();
656 message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
657 @Override
658 public void onMessageFound(Message message) {
659 markMessage(message,Message.STATUS_SEND_FAILED);
660 }
661 });
662 packet = mMessageGenerator.generatePgpChat(message);
663 send = true;
664 } else {
665 message.getConversation().endOtrIfNeeded();
666 message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
667 @Override
668 public void onMessageFound(Message message) {
669 markMessage(message,Message.STATUS_SEND_FAILED);
670 }
671 });
672 packet = mMessageGenerator.generateChat(message);
673 send = true;
674 }
675 }
676 if (!account.getXmppConnection().getFeatures().sm()
677 && conv.getMode() != Conversation.MODE_MULTI) {
678 message.setStatus(Message.STATUS_SEND);
679 }
680 } else {
681 message.setStatus(Message.STATUS_WAITING);
682 if (message.getType() == Message.TYPE_TEXT) {
683 if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
684 String pgpBody = message.getEncryptedBody();
685 String decryptedBody = message.getBody();
686 message.setBody(pgpBody);
687 message.setEncryption(Message.ENCRYPTION_PGP);
688 databaseBackend.createMessage(message);
689 saveInDb = false;
690 message.setBody(decryptedBody);
691 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
692 } else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
693 if (!conv.hasValidOtrSession()
694 && message.getCounterpart() != null) {
695 conv.startOtrSession(message.getCounterpart().getResourcepart(), false);
696 }
697 }
698 }
699
700 }
701 conv.add(message);
702 if (saveInDb) {
703 if (message.getEncryption() == Message.ENCRYPTION_NONE
704 || saveEncryptedMessages()) {
705 databaseBackend.createMessage(message);
706 }
707 }
708 if ((send) && (packet != null)) {
709 sendMessagePacket(account, packet);
710 }
711 updateConversationUi();
712 }
713
714 private void sendUnsentMessages(final Conversation conversation) {
715 conversation.findWaitingMessages(new Conversation.OnMessageFound() {
716
717 @Override
718 public void onMessageFound(Message message) {
719 resendMessage(message);
720 }
721 });
722 }
723
724 private void resendMessage(final Message message) {
725 Account account = message.getConversation().getAccount();
726 MessagePacket packet = null;
727 if (message.getEncryption() == Message.ENCRYPTION_OTR) {
728 Presences presences = message.getConversation().getContact()
729 .getPresences();
730 if (!message.getConversation().hasValidOtrSession()) {
731 if ((message.getCounterpart() != null)
732 && (presences.has(message.getCounterpart().getResourcepart()))) {
733 message.getConversation().startOtrSession(message.getCounterpart().getResourcepart(), true);
734 } else {
735 if (presences.size() == 1) {
736 String presence = presences.asStringArray()[0];
737 message.getConversation().startOtrSession(presence, true);
738 }
739 }
740 } else {
741 if (message.getConversation().getOtrSession()
742 .getSessionStatus() == SessionStatus.ENCRYPTED) {
743 try {
744 message.setCounterpart(Jid.fromSessionID(message.getConversation().getOtrSession().getSessionID()));
745 if (message.getType() == Message.TYPE_TEXT) {
746 packet = mMessageGenerator.generateOtrChat(message,
747 true);
748 } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
749 mJingleConnectionManager.createNewConnection(message);
750 }
751 } catch (final InvalidJidException ignored) {
752
753 }
754 }
755 }
756 } else if (message.getType() == Message.TYPE_TEXT) {
757 if (message.getEncryption() == Message.ENCRYPTION_NONE) {
758 packet = mMessageGenerator.generateChat(message, true);
759 } else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED)
760 || (message.getEncryption() == Message.ENCRYPTION_PGP)) {
761 packet = mMessageGenerator.generatePgpChat(message, true);
762 }
763 } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
764 Contact contact = message.getConversation().getContact();
765 Presences presences = contact.getPresences();
766 if ((message.getCounterpart() != null)
767 && (presences.has(message.getCounterpart().getResourcepart()))) {
768 markMessage(message, Message.STATUS_OFFERED);
769 mJingleConnectionManager.createNewConnection(message);
770 } else {
771 if (presences.size() == 1) {
772 String presence = presences.asStringArray()[0];
773 try {
774 message.setCounterpart(Jid.fromParts(contact.getJid().getLocalpart(), contact.getJid().getDomainpart(), presence));
775 } catch (InvalidJidException e) {
776 return;
777 }
778 markMessage(message, Message.STATUS_OFFERED);
779 mJingleConnectionManager.createNewConnection(message);
780 }
781 }
782 }
783 if (packet != null) {
784 if (!account.getXmppConnection().getFeatures().sm()
785 && message.getConversation().getMode() != Conversation.MODE_MULTI) {
786 markMessage(message, Message.STATUS_SEND);
787 } else {
788 markMessage(message, Message.STATUS_UNSEND);
789 }
790 sendMessagePacket(account, packet);
791 }
792 }
793
794 public void fetchRosterFromServer(final Account account) {
795 final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
796 if (!"".equals(account.getRosterVersion())) {
797 Log.d(Config.LOGTAG, account.getJid().toBareJid()
798 + ": fetching roster version " + account.getRosterVersion());
799 } else {
800 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
801 }
802 iqPacket.query(Xmlns.ROSTER).setAttribute("ver",
803 account.getRosterVersion());
804 account.getXmppConnection().sendIqPacket(iqPacket, mIqParser);
805 }
806
807 public void fetchBookmarks(final Account account) {
808 final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
809 final Element query = iqPacket.query("jabber:iq:private");
810 query.addChild("storage", "storage:bookmarks");
811 final OnIqPacketReceived callback = new OnIqPacketReceived() {
812
813 @Override
814 public void onIqPacketReceived(final Account account, final IqPacket packet) {
815 final Element query = packet.query();
816 final List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
817 final Element storage = query.findChild("storage",
818 "storage:bookmarks");
819 if (storage != null) {
820 for (final Element item : storage.getChildren()) {
821 if (item.getName().equals("conference")) {
822 final Bookmark bookmark = Bookmark.parse(item, account);
823 bookmarks.add(bookmark);
824 Conversation conversation = find(bookmark);
825 if (conversation != null) {
826 conversation.setBookmark(bookmark);
827 } else if (bookmark.autojoin() && bookmark.getJid() != null) {
828 conversation = findOrCreateConversation(
829 account, bookmark.getJid(), true);
830 conversation.setBookmark(bookmark);
831 joinMuc(conversation);
832 }
833 }
834 }
835 }
836 account.setBookmarks(bookmarks);
837 }
838 };
839 sendIqPacket(account, iqPacket, callback);
840 }
841
842 public void pushBookmarks(Account account) {
843 IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
844 Element query = iqPacket.query("jabber:iq:private");
845 Element storage = query.addChild("storage", "storage:bookmarks");
846 for (Bookmark bookmark : account.getBookmarks()) {
847 storage.addChild(bookmark);
848 }
849 sendIqPacket(account, iqPacket, null);
850 }
851
852 public void onPhoneContactsLoaded(final List<Bundle> phoneContacts) {
853 if (mPhoneContactMergerThread != null) {
854 mPhoneContactMergerThread.interrupt();
855 }
856 mPhoneContactMergerThread = new Thread(new Runnable() {
857 @Override
858 public void run() {
859 Log.d(Config.LOGTAG,"start merging phone contacts with roster");
860 for (Account account : accounts) {
861 account.getRoster().clearSystemAccounts();
862 for (Bundle phoneContact : phoneContacts) {
863 if (Thread.interrupted()) {
864 Log.d(Config.LOGTAG,"interrupted merging phone contacts");
865 return;
866 }
867 Jid jid;
868 try {
869 jid = Jid.fromString(phoneContact.getString("jid"));
870 } catch (final InvalidJidException e) {
871 continue;
872 }
873 final Contact contact = account.getRoster().getContact(jid);
874 String systemAccount = phoneContact.getInt("phoneid")
875 + "#"
876 + phoneContact.getString("lookup");
877 contact.setSystemAccount(systemAccount);
878 contact.setPhotoUri(phoneContact.getString("photouri"));
879 getAvatarService().clear(contact);
880 contact.setSystemName(phoneContact.getString("displayname"));
881 }
882 }
883 Log.d(Config.LOGTAG,"finished merging phone contacts");
884 updateAccountUi();
885 }
886 });
887 mPhoneContactMergerThread.start();
888 }
889
890 private void restoreFromDatabase() {
891 synchronized (this.conversations) {
892 final Map<String, Account> accountLookupTable = new Hashtable<>();
893 for (Account account : this.accounts) {
894 accountLookupTable.put(account.getUuid(), account);
895 }
896 this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
897 for (Conversation conversation : this.conversations) {
898 Account account = accountLookupTable.get(conversation.getAccountUuid());
899 conversation.setAccount(account);
900 }
901 new Thread(new Runnable() {
902 @Override
903 public void run() {
904 Log.d(Config.LOGTAG,"restoring roster");
905 for(Account account : accounts) {
906 databaseBackend.readRoster(account.getRoster());
907 }
908 getBitmapCache().evictAll();
909 Looper.prepare();
910 PhoneHelper.loadPhoneContacts(getApplicationContext(),
911 new CopyOnWriteArrayList<Bundle>(),
912 XmppConnectionService.this);
913 Log.d(Config.LOGTAG,"restoring messages");
914 for (Conversation conversation : conversations) {
915 conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
916 checkDeletedFiles(conversation);
917 }
918 mRestoredFromDatabase = true;
919 Log.d(Config.LOGTAG,"restored all messages");
920 updateConversationUi();
921 }
922 }).start();
923 }
924 }
925
926 public List<Conversation> getConversations() {
927 return this.conversations;
928 }
929
930 private void checkDeletedFiles(Conversation conversation) {
931 conversation.findMessagesWithFiles(new Conversation.OnMessageFound() {
932
933 @Override
934 public void onMessageFound(Message message) {
935 if (!getFileBackend().isFileAvailable(message)) {
936 message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED));
937 }
938 }
939 });
940 }
941
942 private void markFileDeleted(String uuid) {
943 for (Conversation conversation : getConversations()) {
944 Message message = conversation.findMessageWithFileAndUuid(uuid);
945 if (message != null) {
946 if (!getFileBackend().isFileAvailable(message)) {
947 message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED));
948 updateConversationUi();
949 }
950 return;
951 }
952 }
953 }
954
955 public void populateWithOrderedConversations(final List<Conversation> list) {
956 populateWithOrderedConversations(list, true);
957 }
958
959 public void populateWithOrderedConversations(final List<Conversation> list, boolean includeConferences) {
960 list.clear();
961 if (includeConferences) {
962 list.addAll(getConversations());
963 } else {
964 for (Conversation conversation : getConversations()) {
965 if (conversation.getMode() == Conversation.MODE_SINGLE) {
966 list.add(conversation);
967 }
968 }
969 }
970 Collections.sort(list, new Comparator<Conversation>() {
971 @Override
972 public int compare(Conversation lhs, Conversation rhs) {
973 Message left = lhs.getLatestMessage();
974 Message right = rhs.getLatestMessage();
975 if (left.getTimeSent() > right.getTimeSent()) {
976 return -1;
977 } else if (left.getTimeSent() < right.getTimeSent()) {
978 return 1;
979 } else {
980 return 0;
981 }
982 }
983 });
984 }
985
986 public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
987 Log.d(Config.LOGTAG,"load more messages for "+conversation.getName() + " prior to "+MessageGenerator.getTimestamp(timestamp));
988 if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation,callback)) {
989 return;
990 }
991 new Thread(new Runnable() {
992 @Override
993 public void run() {
994 final Account account = conversation.getAccount();
995 List<Message> messages = databaseBackend.getMessages(conversation, 50,timestamp);
996 if (messages.size() > 0) {
997 conversation.addAll(0, messages);
998 checkDeletedFiles(conversation);
999 callback.onMoreMessagesLoaded(messages.size(), conversation);
1000 } else if (conversation.hasMessagesLeftOnServer()
1001 && account.isOnlineAndConnected()
1002 && account.getXmppConnection().getFeatures().mam()) {
1003 MessageArchiveService.Query query = getMessageArchiveService().query(conversation,0,timestamp - 1);
1004 if (query != null) {
1005 query.setCallback(callback);
1006 }
1007 callback.informUser(R.string.fetching_history_from_server);
1008 }
1009 }
1010 }).start();
1011 }
1012
1013 public List<Account> getAccounts() {
1014 return this.accounts;
1015 }
1016
1017 public Conversation find(final Iterable<Conversation> haystack, final Contact contact) {
1018 for (final Conversation conversation : haystack) {
1019 if (conversation.getContact() == contact) {
1020 return conversation;
1021 }
1022 }
1023 return null;
1024 }
1025
1026 public Conversation find(final Iterable<Conversation> haystack, final Account account, final Jid jid) {
1027 if (jid == null) {
1028 return null;
1029 }
1030 for (final Conversation conversation : haystack) {
1031 if ((account == null || conversation.getAccount() == account)
1032 && (conversation.getJid().toBareJid().equals(jid.toBareJid()))) {
1033 return conversation;
1034 }
1035 }
1036 return null;
1037 }
1038
1039 public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc) {
1040 return this.findOrCreateConversation(account, jid, muc, null);
1041 }
1042
1043 public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final MessageArchiveService.Query query) {
1044 synchronized (this.conversations) {
1045 Conversation conversation = find(account, jid);
1046 if (conversation != null) {
1047 return conversation;
1048 }
1049 conversation = databaseBackend.findConversation(account, jid);
1050 if (conversation != null) {
1051 conversation.setStatus(Conversation.STATUS_AVAILABLE);
1052 conversation.setAccount(account);
1053 if (muc) {
1054 conversation.setMode(Conversation.MODE_MULTI);
1055 conversation.setContactJid(jid);
1056 } else {
1057 conversation.setMode(Conversation.MODE_SINGLE);
1058 conversation.setContactJid(jid.toBareJid());
1059 }
1060 conversation.setNextEncryption(-1);
1061 conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
1062 this.databaseBackend.updateConversation(conversation);
1063 } else {
1064 String conversationName;
1065 Contact contact = account.getRoster().getContact(jid);
1066 if (contact != null) {
1067 conversationName = contact.getDisplayName();
1068 } else {
1069 conversationName = jid.getLocalpart();
1070 }
1071 if (muc) {
1072 conversation = new Conversation(conversationName, account, jid,
1073 Conversation.MODE_MULTI);
1074 } else {
1075 conversation = new Conversation(conversationName, account, jid.toBareJid(),
1076 Conversation.MODE_SINGLE);
1077 }
1078 this.databaseBackend.createConversation(conversation);
1079 }
1080 if (account.getXmppConnection() != null
1081 && account.getXmppConnection().getFeatures().mam()
1082 && !muc) {
1083 if (query == null) {
1084 this.mMessageArchiveService.query(conversation);
1085 } else {
1086 if (query.getConversation() == null) {
1087 this.mMessageArchiveService.query(conversation, query.getStart());
1088 }
1089 }
1090 }
1091 checkDeletedFiles(conversation);
1092 this.conversations.add(conversation);
1093 updateConversationUi();
1094 return conversation;
1095 }
1096 }
1097
1098 public void archiveConversation(Conversation conversation) {
1099 conversation.setStatus(Conversation.STATUS_ARCHIVED);
1100 conversation.setNextEncryption(-1);
1101 synchronized (this.conversations) {
1102 if (conversation.getMode() == Conversation.MODE_MULTI) {
1103 if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1104 Bookmark bookmark = conversation.getBookmark();
1105 if (bookmark != null && bookmark.autojoin()) {
1106 bookmark.setAutojoin(false);
1107 pushBookmarks(bookmark.getAccount());
1108 }
1109 }
1110 leaveMuc(conversation);
1111 } else {
1112 conversation.endOtrIfNeeded();
1113 }
1114 this.databaseBackend.updateConversation(conversation);
1115 this.conversations.remove(conversation);
1116 updateConversationUi();
1117 }
1118 }
1119
1120 public void createAccount(final Account account) {
1121 account.initOtrEngine(this);
1122 databaseBackend.createAccount(account);
1123 this.accounts.add(account);
1124 this.reconnectAccount(account, false);
1125 updateAccountUi();
1126 }
1127
1128 public void updateAccount(final Account account) {
1129 this.statusListener.onStatusChanged(account);
1130 databaseBackend.updateAccount(account);
1131 reconnectAccount(account, false);
1132 updateAccountUi();
1133 getNotificationService().updateErrorNotification();
1134 }
1135
1136 public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) {
1137 final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword);
1138 sendIqPacket(account, iq, new OnIqPacketReceived() {
1139 @Override
1140 public void onIqPacketReceived(final Account account, final IqPacket packet) {
1141 if (packet.getType() == IqPacket.TYPE.RESULT) {
1142 account.setPassword(newPassword);
1143 databaseBackend.updateAccount(account);
1144 callback.onPasswordChangeSucceeded();
1145 } else {
1146 callback.onPasswordChangeFailed();
1147 }
1148 }
1149 });
1150 }
1151
1152 public void deleteAccount(final Account account) {
1153 synchronized (this.conversations) {
1154 for (final Conversation conversation : conversations) {
1155 if (conversation.getAccount() == account) {
1156 if (conversation.getMode() == Conversation.MODE_MULTI) {
1157 leaveMuc(conversation);
1158 } else if (conversation.getMode() == Conversation.MODE_SINGLE) {
1159 conversation.endOtrIfNeeded();
1160 }
1161 conversations.remove(conversation);
1162 }
1163 }
1164 if (account.getXmppConnection() != null) {
1165 this.disconnect(account, true);
1166 }
1167 databaseBackend.deleteAccount(account);
1168 this.accounts.remove(account);
1169 updateAccountUi();
1170 getNotificationService().updateErrorNotification();
1171 }
1172 }
1173
1174 public void setOnConversationListChangedListener(OnConversationUpdate listener) {
1175 synchronized (this) {
1176 if (checkListeners()) {
1177 switchToForeground();
1178 }
1179 this.mOnConversationUpdate = listener;
1180 this.mNotificationService.setIsInForeground(true);
1181 if (this.convChangedListenerCount < 2) {
1182 this.convChangedListenerCount++;
1183 }
1184 }
1185 }
1186
1187 public void removeOnConversationListChangedListener() {
1188 synchronized (this) {
1189 this.convChangedListenerCount--;
1190 if (this.convChangedListenerCount <= 0) {
1191 this.convChangedListenerCount = 0;
1192 this.mOnConversationUpdate = null;
1193 this.mNotificationService.setIsInForeground(false);
1194 if (checkListeners()) {
1195 switchToBackground();
1196 }
1197 }
1198 }
1199 }
1200
1201 public void setOnAccountListChangedListener(OnAccountUpdate listener) {
1202 synchronized (this) {
1203 if (checkListeners()) {
1204 switchToForeground();
1205 }
1206 this.mOnAccountUpdate = listener;
1207 if (this.accountChangedListenerCount < 2) {
1208 this.accountChangedListenerCount++;
1209 }
1210 }
1211 }
1212
1213 public void removeOnAccountListChangedListener() {
1214 synchronized (this) {
1215 this.accountChangedListenerCount--;
1216 if (this.accountChangedListenerCount <= 0) {
1217 this.mOnAccountUpdate = null;
1218 this.accountChangedListenerCount = 0;
1219 if (checkListeners()) {
1220 switchToBackground();
1221 }
1222 }
1223 }
1224 }
1225
1226 public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
1227 synchronized (this) {
1228 if (checkListeners()) {
1229 switchToForeground();
1230 }
1231 this.mOnRosterUpdate = listener;
1232 if (this.rosterChangedListenerCount < 2) {
1233 this.rosterChangedListenerCount++;
1234 }
1235 }
1236 }
1237
1238 public void removeOnRosterUpdateListener() {
1239 synchronized (this) {
1240 this.rosterChangedListenerCount--;
1241 if (this.rosterChangedListenerCount <= 0) {
1242 this.rosterChangedListenerCount = 0;
1243 this.mOnRosterUpdate = null;
1244 if (checkListeners()) {
1245 switchToBackground();
1246 }
1247 }
1248 }
1249 }
1250
1251 public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
1252 synchronized (this) {
1253 if (checkListeners()) {
1254 switchToForeground();
1255 }
1256 this.mOnUpdateBlocklist = listener;
1257 if (this.updateBlocklistListenerCount < 2) {
1258 this.updateBlocklistListenerCount++;
1259 }
1260 }
1261 }
1262
1263 public void removeOnUpdateBlocklistListener() {
1264 synchronized (this) {
1265 this.updateBlocklistListenerCount--;
1266 if (this.updateBlocklistListenerCount <= 0) {
1267 this.updateBlocklistListenerCount = 0;
1268 this.mOnUpdateBlocklist = null;
1269 if (checkListeners()) {
1270 switchToBackground();
1271 }
1272 }
1273 }
1274 }
1275
1276 public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
1277 synchronized (this) {
1278 if (checkListeners()) {
1279 switchToForeground();
1280 }
1281 this.mOnMucRosterUpdate = listener;
1282 if (this.mucRosterChangedListenerCount < 2) {
1283 this.mucRosterChangedListenerCount++;
1284 }
1285 }
1286 }
1287
1288 public void removeOnMucRosterUpdateListener() {
1289 synchronized (this) {
1290 this.mucRosterChangedListenerCount--;
1291 if (this.mucRosterChangedListenerCount <= 0) {
1292 this.mucRosterChangedListenerCount = 0;
1293 this.mOnMucRosterUpdate = null;
1294 if (checkListeners()) {
1295 switchToBackground();
1296 }
1297 }
1298 }
1299 }
1300
1301 private boolean checkListeners() {
1302 return (this.mOnAccountUpdate == null
1303 && this.mOnConversationUpdate == null
1304 && this.mOnRosterUpdate == null
1305 && this.mOnUpdateBlocklist == null);
1306 }
1307
1308 private void switchToForeground() {
1309 for (Account account : getAccounts()) {
1310 if (account.getStatus() == Account.State.ONLINE) {
1311 XmppConnection connection = account.getXmppConnection();
1312 if (connection != null && connection.getFeatures().csi()) {
1313 connection.sendActive();
1314 }
1315 }
1316 }
1317 Log.d(Config.LOGTAG, "app switched into foreground");
1318 }
1319
1320 private void switchToBackground() {
1321 for (Account account : getAccounts()) {
1322 if (account.getStatus() == Account.State.ONLINE) {
1323 XmppConnection connection = account.getXmppConnection();
1324 if (connection != null && connection.getFeatures().csi()) {
1325 connection.sendInactive();
1326 }
1327 }
1328 }
1329 this.mNotificationService.setIsInForeground(false);
1330 Log.d(Config.LOGTAG, "app switched into background");
1331 }
1332
1333 private void connectMultiModeConversations(Account account) {
1334 List<Conversation> conversations = getConversations();
1335 for (Conversation conversation : conversations) {
1336 if ((conversation.getMode() == Conversation.MODE_MULTI)
1337 && (conversation.getAccount() == account)) {
1338 conversation.resetMucOptions();
1339 joinMuc(conversation);
1340 }
1341 }
1342 }
1343
1344 public void joinMuc(Conversation conversation) {
1345 Account account = conversation.getAccount();
1346 account.pendingConferenceJoins.remove(conversation);
1347 account.pendingConferenceLeaves.remove(conversation);
1348 if (account.getStatus() == Account.State.ONLINE) {
1349 final String nick = conversation.getMucOptions().getProposedNick();
1350 final Jid joinJid = conversation.getMucOptions().createJoinJid(nick);
1351 if (joinJid == null) {
1352 return; //safety net
1353 }
1354 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString());
1355 PresencePacket packet = new PresencePacket();
1356 packet.setFrom(conversation.getAccount().getJid());
1357 packet.setTo(joinJid);
1358 Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
1359 if (conversation.getMucOptions().getPassword() != null) {
1360 x.addChild("password").setContent(conversation.getMucOptions().getPassword());
1361 }
1362 x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted()));
1363 String sig = account.getPgpSignature();
1364 if (sig != null) {
1365 packet.addChild("status").setContent("online");
1366 packet.addChild("x", "jabber:x:signed").setContent(sig);
1367 }
1368 sendPresencePacket(account, packet);
1369 fetchConferenceConfiguration(conversation);
1370 if (!joinJid.equals(conversation.getJid())) {
1371 conversation.setContactJid(joinJid);
1372 databaseBackend.updateConversation(conversation);
1373 }
1374 conversation.setHasMessagesLeftOnServer(false);
1375 } else {
1376 account.pendingConferenceJoins.add(conversation);
1377 }
1378 }
1379
1380 public void providePasswordForMuc(Conversation conversation, String password) {
1381 if (conversation.getMode() == Conversation.MODE_MULTI) {
1382 conversation.getMucOptions().setPassword(password);
1383 if (conversation.getBookmark() != null) {
1384 conversation.getBookmark().setAutojoin(true);
1385 pushBookmarks(conversation.getAccount());
1386 }
1387 databaseBackend.updateConversation(conversation);
1388 joinMuc(conversation);
1389 }
1390 }
1391
1392 public void renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
1393 final MucOptions options = conversation.getMucOptions();
1394 final Jid joinJid = options.createJoinJid(nick);
1395 if (options.online()) {
1396 Account account = conversation.getAccount();
1397 options.setOnRenameListener(new OnRenameListener() {
1398
1399 @Override
1400 public void onSuccess() {
1401 conversation.setContactJid(joinJid);
1402 databaseBackend.updateConversation(conversation);
1403 Bookmark bookmark = conversation.getBookmark();
1404 if (bookmark != null) {
1405 bookmark.setNick(nick);
1406 pushBookmarks(bookmark.getAccount());
1407 }
1408 callback.success(conversation);
1409 }
1410
1411 @Override
1412 public void onFailure() {
1413 callback.error(R.string.nick_in_use, conversation);
1414 }
1415 });
1416
1417 PresencePacket packet = new PresencePacket();
1418 packet.setTo(joinJid);
1419 packet.setFrom(conversation.getAccount().getJid());
1420
1421 String sig = account.getPgpSignature();
1422 if (sig != null) {
1423 packet.addChild("status").setContent("online");
1424 packet.addChild("x", "jabber:x:signed").setContent(sig);
1425 }
1426 sendPresencePacket(account, packet);
1427 } else {
1428 conversation.setContactJid(joinJid);
1429 databaseBackend.updateConversation(conversation);
1430 if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1431 Bookmark bookmark = conversation.getBookmark();
1432 if (bookmark != null) {
1433 bookmark.setNick(nick);
1434 pushBookmarks(bookmark.getAccount());
1435 }
1436 joinMuc(conversation);
1437 }
1438 }
1439 }
1440
1441 public void leaveMuc(Conversation conversation) {
1442 Account account = conversation.getAccount();
1443 account.pendingConferenceJoins.remove(conversation);
1444 account.pendingConferenceLeaves.remove(conversation);
1445 if (account.getStatus() == Account.State.ONLINE) {
1446 PresencePacket packet = new PresencePacket();
1447 packet.setTo(conversation.getJid());
1448 packet.setFrom(conversation.getAccount().getJid());
1449 packet.setAttribute("type", "unavailable");
1450 sendPresencePacket(conversation.getAccount(), packet);
1451 conversation.getMucOptions().setOffline();
1452 conversation.deregisterWithBookmark();
1453 Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
1454 + ": leaving muc " + conversation.getJid());
1455 } else {
1456 account.pendingConferenceLeaves.add(conversation);
1457 }
1458 }
1459
1460 private String findConferenceServer(final Account account) {
1461 String server;
1462 if (account.getXmppConnection() != null) {
1463 server = account.getXmppConnection().getMucServer();
1464 if (server != null) {
1465 return server;
1466 }
1467 }
1468 for (Account other : getAccounts()) {
1469 if (other != account && other.getXmppConnection() != null) {
1470 server = other.getXmppConnection().getMucServer();
1471 if (server != null) {
1472 return server;
1473 }
1474 }
1475 }
1476 return null;
1477 }
1478
1479 public void createAdhocConference(final Account account, final Iterable<Jid> jids, final UiCallback<Conversation> callback) {
1480 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString());
1481 if (account.getStatus() == Account.State.ONLINE) {
1482 try {
1483 String server = findConferenceServer(account);
1484 if (server == null) {
1485 if (callback != null) {
1486 callback.error(R.string.no_conference_server_found, null);
1487 }
1488 return;
1489 }
1490 String name = new BigInteger(75, getRNG()).toString(32);
1491 Jid jid = Jid.fromParts(name, server, null);
1492 final Conversation conversation = findOrCreateConversation(account, jid, true);
1493 joinMuc(conversation);
1494 Bundle options = new Bundle();
1495 options.putString("muc#roomconfig_persistentroom", "1");
1496 options.putString("muc#roomconfig_membersonly", "1");
1497 options.putString("muc#roomconfig_publicroom", "0");
1498 options.putString("muc#roomconfig_whois", "anyone");
1499 pushConferenceConfiguration(conversation, options, new OnConferenceOptionsPushed() {
1500 @Override
1501 public void onPushSucceeded() {
1502 for (Jid invite : jids) {
1503 invite(conversation, invite);
1504 }
1505 if (callback != null) {
1506 callback.success(conversation);
1507 }
1508 }
1509
1510 @Override
1511 public void onPushFailed() {
1512 if (callback != null) {
1513 callback.error(R.string.conference_creation_failed, conversation);
1514 }
1515 }
1516 });
1517
1518 } catch (InvalidJidException e) {
1519 if (callback != null) {
1520 callback.error(R.string.conference_creation_failed, null);
1521 }
1522 }
1523 } else {
1524 if (callback != null) {
1525 callback.error(R.string.not_connected_try_again, null);
1526 }
1527 }
1528 }
1529
1530 public void fetchConferenceConfiguration(final Conversation conversation) {
1531 IqPacket request = new IqPacket(IqPacket.TYPE.GET);
1532 request.setTo(conversation.getJid().toBareJid());
1533 request.query("http://jabber.org/protocol/disco#info");
1534 sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
1535 @Override
1536 public void onIqPacketReceived(Account account, IqPacket packet) {
1537 if (packet.getType() != IqPacket.TYPE.ERROR) {
1538 ArrayList<String> features = new ArrayList<>();
1539 for (Element child : packet.query().getChildren()) {
1540 if (child != null && child.getName().equals("feature")) {
1541 String var = child.getAttribute("var");
1542 if (var != null) {
1543 features.add(var);
1544 }
1545 }
1546 }
1547 conversation.getMucOptions().updateFeatures(features);
1548 updateConversationUi();
1549 }
1550 }
1551 });
1552 }
1553
1554 public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConferenceOptionsPushed callback) {
1555 IqPacket request = new IqPacket(IqPacket.TYPE.GET);
1556 request.setTo(conversation.getJid().toBareJid());
1557 request.query("http://jabber.org/protocol/muc#owner");
1558 sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
1559 @Override
1560 public void onIqPacketReceived(Account account, IqPacket packet) {
1561 if (packet.getType() != IqPacket.TYPE.ERROR) {
1562 Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
1563 for (Field field : data.getFields()) {
1564 if (options.containsKey(field.getName())) {
1565 field.setValue(options.getString(field.getName()));
1566 }
1567 }
1568 data.submit();
1569 IqPacket set = new IqPacket(IqPacket.TYPE.SET);
1570 set.setTo(conversation.getJid().toBareJid());
1571 set.query("http://jabber.org/protocol/muc#owner").addChild(data);
1572 sendIqPacket(account, set, new OnIqPacketReceived() {
1573 @Override
1574 public void onIqPacketReceived(Account account, IqPacket packet) {
1575 if (packet.getType() == IqPacket.TYPE.RESULT) {
1576 if (callback != null) {
1577 callback.onPushSucceeded();
1578 }
1579 } else {
1580 if (callback != null) {
1581 callback.onPushFailed();
1582 }
1583 }
1584 }
1585 });
1586 } else {
1587 if (callback != null) {
1588 callback.onPushFailed();
1589 }
1590 }
1591 }
1592 });
1593 }
1594
1595 public void pushSubjectToConference(final Conversation conference, final String subject) {
1596 MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, subject);
1597 this.sendMessagePacket(conference.getAccount(), packet);
1598 final MucOptions mucOptions = conference.getMucOptions();
1599 final MucOptions.User self = mucOptions.getSelf();
1600 if (!mucOptions.persistent() && self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
1601 Bundle options = new Bundle();
1602 options.putString("muc#roomconfig_persistentroom", "1");
1603 this.pushConferenceConfiguration(conference, options, null);
1604 }
1605 }
1606
1607 public void changeAffiliationInConference(final Conversation conference, Jid user, MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
1608 final Jid jid = user.toBareJid();
1609 IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
1610 sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
1611 @Override
1612 public void onIqPacketReceived(Account account, IqPacket packet) {
1613 if (packet.getType() == IqPacket.TYPE.RESULT) {
1614 callback.onAffiliationChangedSuccessful(jid);
1615 } else {
1616 callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
1617 }
1618 }
1619 });
1620 }
1621
1622 public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
1623 List<Jid> jids = new ArrayList<>();
1624 for (MucOptions.User user : conference.getMucOptions().getUsers()) {
1625 if (user.getAffiliation() == before) {
1626 jids.add(user.getJid());
1627 }
1628 }
1629 IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
1630 sendIqPacket(conference.getAccount(), request, null);
1631 }
1632
1633 public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) {
1634 IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
1635 Log.d(Config.LOGTAG, request.toString());
1636 sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
1637 @Override
1638 public void onIqPacketReceived(Account account, IqPacket packet) {
1639 Log.d(Config.LOGTAG, packet.toString());
1640 if (packet.getType() == IqPacket.TYPE.RESULT) {
1641 callback.onRoleChangedSuccessful(nick);
1642 } else {
1643 callback.onRoleChangeFailed(nick, R.string.could_not_change_role);
1644 }
1645 }
1646 });
1647 }
1648
1649 public void disconnect(Account account, boolean force) {
1650 if ((account.getStatus() == Account.State.ONLINE)
1651 || (account.getStatus() == Account.State.DISABLED)) {
1652 if (!force) {
1653 List<Conversation> conversations = getConversations();
1654 for (Conversation conversation : conversations) {
1655 if (conversation.getAccount() == account) {
1656 if (conversation.getMode() == Conversation.MODE_MULTI) {
1657 leaveMuc(conversation);
1658 } else {
1659 if (conversation.endOtrIfNeeded()) {
1660 Log.d(Config.LOGTAG, account.getJid().toBareJid()
1661 + ": ended otr session with "
1662 + conversation.getJid());
1663 }
1664 }
1665 }
1666 }
1667 }
1668 account.getXmppConnection().disconnect(force);
1669 }
1670 }
1671
1672 @Override
1673 public IBinder onBind(Intent intent) {
1674 return mBinder;
1675 }
1676
1677 public void updateMessage(Message message) {
1678 databaseBackend.updateMessage(message);
1679 updateConversationUi();
1680 }
1681
1682 protected void syncDirtyContacts(Account account) {
1683 for (Contact contact : account.getRoster().getContacts()) {
1684 if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
1685 pushContactToServer(contact);
1686 }
1687 if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
1688 deleteContactOnServer(contact);
1689 }
1690 }
1691 }
1692
1693 public void createContact(Contact contact) {
1694 SharedPreferences sharedPref = getPreferences();
1695 boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1696 if (autoGrant) {
1697 contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
1698 contact.setOption(Contact.Options.ASKING);
1699 }
1700 pushContactToServer(contact);
1701 }
1702
1703 public void onOtrSessionEstablished(Conversation conversation) {
1704 final Account account = conversation.getAccount();
1705 final Session otrSession = conversation.getOtrSession();
1706 Log.d(Config.LOGTAG,
1707 account.getJid().toBareJid() + " otr session established with "
1708 + conversation.getJid() + "/"
1709 + otrSession.getSessionID().getUserID());
1710 conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
1711
1712 @Override
1713 public void onMessageFound(Message message) {
1714 SessionID id = otrSession.getSessionID();
1715 try {
1716 message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
1717 } catch (InvalidJidException e) {
1718 return;
1719 }
1720 if (message.getType() == Message.TYPE_TEXT) {
1721 MessagePacket outPacket = mMessageGenerator.generateOtrChat(message, true);
1722 if (outPacket != null) {
1723 message.setStatus(Message.STATUS_SEND);
1724 databaseBackend.updateMessage(message);
1725 sendMessagePacket(account, outPacket);
1726 }
1727 } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
1728 mJingleConnectionManager.createNewConnection(message);
1729 }
1730 updateConversationUi();
1731 }
1732 });
1733 }
1734
1735 public boolean renewSymmetricKey(Conversation conversation) {
1736 Account account = conversation.getAccount();
1737 byte[] symmetricKey = new byte[32];
1738 this.mRandom.nextBytes(symmetricKey);
1739 Session otrSession = conversation.getOtrSession();
1740 if (otrSession != null) {
1741 MessagePacket packet = new MessagePacket();
1742 packet.setType(MessagePacket.TYPE_CHAT);
1743 packet.setFrom(account.getJid());
1744 packet.addChild("private", "urn:xmpp:carbons:2");
1745 packet.addChild("no-copy", "urn:xmpp:hints");
1746 packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
1747 + otrSession.getSessionID().getUserID());
1748 try {
1749 packet.setBody(otrSession
1750 .transformSending(CryptoHelper.FILETRANSFER
1751 + CryptoHelper.bytesToHex(symmetricKey))[0]);
1752 sendMessagePacket(account, packet);
1753 conversation.setSymmetricKey(symmetricKey);
1754 return true;
1755 } catch (OtrException e) {
1756 return false;
1757 }
1758 }
1759 return false;
1760 }
1761
1762 public void pushContactToServer(final Contact contact) {
1763 contact.resetOption(Contact.Options.DIRTY_DELETE);
1764 contact.setOption(Contact.Options.DIRTY_PUSH);
1765 final Account account = contact.getAccount();
1766 if (account.getStatus() == Account.State.ONLINE) {
1767 final boolean ask = contact.getOption(Contact.Options.ASKING);
1768 final boolean sendUpdates = contact
1769 .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
1770 && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
1771 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1772 iq.query(Xmlns.ROSTER).addChild(contact.asElement());
1773 account.getXmppConnection().sendIqPacket(iq, null);
1774 if (sendUpdates) {
1775 sendPresencePacket(account,
1776 mPresenceGenerator.sendPresenceUpdatesTo(contact));
1777 }
1778 if (ask) {
1779 sendPresencePacket(account,
1780 mPresenceGenerator.requestPresenceUpdatesFrom(contact));
1781 }
1782 }
1783 }
1784
1785 public void publishAvatar(final Account account,
1786 final Uri image,
1787 final UiCallback<Avatar> callback) {
1788 final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
1789 final int size = Config.AVATAR_SIZE;
1790 final Avatar avatar = getFileBackend()
1791 .getPepAvatar(image, size, format);
1792 if (avatar != null) {
1793 avatar.height = size;
1794 avatar.width = size;
1795 if (format.equals(Bitmap.CompressFormat.WEBP)) {
1796 avatar.type = "image/webp";
1797 } else if (format.equals(Bitmap.CompressFormat.JPEG)) {
1798 avatar.type = "image/jpeg";
1799 } else if (format.equals(Bitmap.CompressFormat.PNG)) {
1800 avatar.type = "image/png";
1801 }
1802 if (!getFileBackend().save(avatar)) {
1803 callback.error(R.string.error_saving_avatar, avatar);
1804 return;
1805 }
1806 final IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
1807 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1808
1809 @Override
1810 public void onIqPacketReceived(Account account, IqPacket result) {
1811 if (result.getType() == IqPacket.TYPE.RESULT) {
1812 final IqPacket packet = XmppConnectionService.this.mIqGenerator
1813 .publishAvatarMetadata(avatar);
1814 sendIqPacket(account, packet, new OnIqPacketReceived() {
1815
1816 @Override
1817 public void onIqPacketReceived(Account account,
1818 IqPacket result) {
1819 if (result.getType() == IqPacket.TYPE.RESULT) {
1820 if (account.setAvatar(avatar.getFilename())) {
1821 databaseBackend.updateAccount(account);
1822 }
1823 callback.success(avatar);
1824 } else {
1825 callback.error(
1826 R.string.error_publish_avatar_server_reject,
1827 avatar);
1828 }
1829 }
1830 });
1831 } else {
1832 callback.error(
1833 R.string.error_publish_avatar_server_reject,
1834 avatar);
1835 }
1836 }
1837 });
1838 } else {
1839 callback.error(R.string.error_publish_avatar_converting, null);
1840 }
1841 }
1842
1843 public void fetchAvatar(Account account, Avatar avatar) {
1844 fetchAvatar(account, avatar, null);
1845 }
1846
1847 public void fetchAvatar(Account account, final Avatar avatar,
1848 final UiCallback<Avatar> callback) {
1849 IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar);
1850 sendIqPacket(account, packet, new OnIqPacketReceived() {
1851
1852 @Override
1853 public void onIqPacketReceived(Account account, IqPacket result) {
1854 final String ERROR = account.getJid().toBareJid()
1855 + ": fetching avatar for " + avatar.owner + " failed ";
1856 if (result.getType() == IqPacket.TYPE.RESULT) {
1857 avatar.image = mIqParser.avatarData(result);
1858 if (avatar.image != null) {
1859 if (getFileBackend().save(avatar)) {
1860 if (account.getJid().toBareJid().equals(avatar.owner)) {
1861 if (account.setAvatar(avatar.getFilename())) {
1862 databaseBackend.updateAccount(account);
1863 }
1864 getAvatarService().clear(account);
1865 updateConversationUi();
1866 updateAccountUi();
1867 } else {
1868 Contact contact = account.getRoster()
1869 .getContact(avatar.owner);
1870 contact.setAvatar(avatar.getFilename());
1871 getAvatarService().clear(contact);
1872 updateConversationUi();
1873 updateRosterUi();
1874 }
1875 if (callback != null) {
1876 callback.success(avatar);
1877 }
1878 Log.d(Config.LOGTAG, account.getJid().toBareJid()
1879 + ": succesfully fetched avatar for "
1880 + avatar.owner);
1881 return;
1882 }
1883 } else {
1884
1885 Log.d(Config.LOGTAG, ERROR + "(parsing error)");
1886 }
1887 } else {
1888 Element error = result.findChild("error");
1889 if (error == null) {
1890 Log.d(Config.LOGTAG, ERROR + "(server error)");
1891 } else {
1892 Log.d(Config.LOGTAG, ERROR + error.toString());
1893 }
1894 }
1895 if (callback != null) {
1896 callback.error(0, null);
1897 }
1898
1899 }
1900 });
1901 }
1902
1903 public void checkForAvatar(Account account,
1904 final UiCallback<Avatar> callback) {
1905 IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
1906 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1907
1908 @Override
1909 public void onIqPacketReceived(Account account, IqPacket packet) {
1910 if (packet.getType() == IqPacket.TYPE.RESULT) {
1911 Element pubsub = packet.findChild("pubsub",
1912 "http://jabber.org/protocol/pubsub");
1913 if (pubsub != null) {
1914 Element items = pubsub.findChild("items");
1915 if (items != null) {
1916 Avatar avatar = Avatar.parseMetadata(items);
1917 if (avatar != null) {
1918 avatar.owner = account.getJid().toBareJid();
1919 if (fileBackend.isAvatarCached(avatar)) {
1920 if (account.setAvatar(avatar.getFilename())) {
1921 databaseBackend.updateAccount(account);
1922 }
1923 getAvatarService().clear(account);
1924 callback.success(avatar);
1925 } else {
1926 fetchAvatar(account, avatar, callback);
1927 }
1928 return;
1929 }
1930 }
1931 }
1932 }
1933 callback.error(0, null);
1934 }
1935 });
1936 }
1937
1938 public void deleteContactOnServer(Contact contact) {
1939 contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
1940 contact.resetOption(Contact.Options.DIRTY_PUSH);
1941 contact.setOption(Contact.Options.DIRTY_DELETE);
1942 Account account = contact.getAccount();
1943 if (account.getStatus() == Account.State.ONLINE) {
1944 IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1945 Element item = iq.query(Xmlns.ROSTER).addChild("item");
1946 item.setAttribute("jid", contact.getJid().toString());
1947 item.setAttribute("subscription", "remove");
1948 account.getXmppConnection().sendIqPacket(iq, null);
1949 }
1950 }
1951
1952 public void updateConversation(Conversation conversation) {
1953 this.databaseBackend.updateConversation(conversation);
1954 }
1955
1956 public void reconnectAccount(final Account account, final boolean force) {
1957 new Thread(new Runnable() {
1958
1959 @Override
1960 public void run() {
1961 if (account.getXmppConnection() != null) {
1962 disconnect(account, force);
1963 }
1964 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1965 if (account.getXmppConnection() == null) {
1966 account.setXmppConnection(createConnection(account));
1967 }
1968 Thread thread = new Thread(account.getXmppConnection());
1969 thread.start();
1970 scheduleWakeUpCall(Config.CONNECT_TIMEOUT, account.getUuid().hashCode());
1971 } else {
1972 account.getRoster().clearPresences();
1973 account.setXmppConnection(null);
1974 }
1975 }
1976 }).start();
1977 }
1978
1979 public void invite(Conversation conversation, Jid contact) {
1980 MessagePacket packet = mMessageGenerator.invite(conversation, contact);
1981 sendMessagePacket(conversation.getAccount(), packet);
1982 }
1983
1984 public void resetSendingToWaiting(Account account) {
1985 for (Conversation conversation : getConversations()) {
1986 if (conversation.getAccount() == account) {
1987 conversation.findUnsentTextMessages(new Conversation.OnMessageFound() {
1988
1989 @Override
1990 public void onMessageFound(Message message) {
1991 markMessage(message, Message.STATUS_WAITING);
1992 }
1993 });
1994 }
1995 }
1996 }
1997
1998 public boolean markMessage(final Account account, final Jid recipient, final String uuid,
1999 final int status) {
2000 if (uuid == null) {
2001 return false;
2002 } else {
2003 for (Conversation conversation : getConversations()) {
2004 if (conversation.getJid().equals(recipient)
2005 && conversation.getAccount().equals(account)) {
2006 return markMessage(conversation, uuid, status);
2007 }
2008 }
2009 return false;
2010 }
2011 }
2012
2013 public boolean markMessage(Conversation conversation, String uuid,
2014 int status) {
2015 if (uuid == null) {
2016 return false;
2017 } else {
2018 Message message = conversation.findSentMessageWithUuid(uuid);
2019 if (message != null) {
2020 markMessage(message, status);
2021 return true;
2022 } else {
2023 return false;
2024 }
2025 }
2026 }
2027
2028 public void markMessage(Message message, int status) {
2029 if (status == Message.STATUS_SEND_FAILED
2030 && (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
2031 .getStatus() == Message.STATUS_SEND_DISPLAYED)) {
2032 return;
2033 }
2034 message.setStatus(status);
2035 databaseBackend.updateMessage(message);
2036 updateConversationUi();
2037 }
2038
2039 public SharedPreferences getPreferences() {
2040 return PreferenceManager
2041 .getDefaultSharedPreferences(getApplicationContext());
2042 }
2043
2044 public boolean forceEncryption() {
2045 return getPreferences().getBoolean("force_encryption", false);
2046 }
2047
2048 public boolean confirmMessages() {
2049 return getPreferences().getBoolean("confirm_messages", true);
2050 }
2051
2052 public boolean saveEncryptedMessages() {
2053 return !getPreferences().getBoolean("dont_save_encrypted", false);
2054 }
2055
2056 public boolean indicateReceived() {
2057 return getPreferences().getBoolean("indicate_received", false);
2058 }
2059
2060 public void updateConversationUi() {
2061 if (mOnConversationUpdate != null) {
2062 mOnConversationUpdate.onConversationUpdate();
2063 }
2064 }
2065
2066 public void updateAccountUi() {
2067 if (mOnAccountUpdate != null) {
2068 mOnAccountUpdate.onAccountUpdate();
2069 }
2070 }
2071
2072 public void updateRosterUi() {
2073 if (mOnRosterUpdate != null) {
2074 mOnRosterUpdate.onRosterUpdate();
2075 }
2076 }
2077
2078 public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
2079 if (mOnUpdateBlocklist != null) {
2080 mOnUpdateBlocklist.OnUpdateBlocklist(status);
2081 }
2082 }
2083
2084 public void updateMucRosterUi() {
2085 if (mOnMucRosterUpdate != null) {
2086 mOnMucRosterUpdate.onMucRosterUpdate();
2087 }
2088 }
2089
2090 public Account findAccountByJid(final Jid accountJid) {
2091 for (Account account : this.accounts) {
2092 if (account.getJid().toBareJid().equals(accountJid.toBareJid())) {
2093 return account;
2094 }
2095 }
2096 return null;
2097 }
2098
2099 public Conversation findConversationByUuid(String uuid) {
2100 for (Conversation conversation : getConversations()) {
2101 if (conversation.getUuid().equals(uuid)) {
2102 return conversation;
2103 }
2104 }
2105 return null;
2106 }
2107
2108 public void markRead(final Conversation conversation) {
2109 mNotificationService.clear(conversation);
2110 conversation.markRead();
2111 }
2112
2113 public void sendReadMarker(final Conversation conversation) {
2114 final Message markable = conversation.getLatestMarkableMessage();
2115 this.markRead(conversation);
2116 if (confirmMessages() && markable != null && markable.getRemoteMsgId() != null) {
2117 Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
2118 Account account = conversation.getAccount();
2119 final Jid to = markable.getCounterpart();
2120 MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId());
2121 this.sendMessagePacket(conversation.getAccount(), packet);
2122 }
2123 updateConversationUi();
2124 }
2125
2126 public SecureRandom getRNG() {
2127 return this.mRandom;
2128 }
2129
2130 public MemorizingTrustManager getMemorizingTrustManager() {
2131 return this.mMemorizingTrustManager;
2132 }
2133
2134 public PowerManager getPowerManager() {
2135 return this.pm;
2136 }
2137
2138 public LruCache<String, Bitmap> getBitmapCache() {
2139 return this.mBitmapCache;
2140 }
2141
2142 public void syncRosterToDisk(final Account account) {
2143 new Thread(new Runnable() {
2144
2145 @Override
2146 public void run() {
2147 databaseBackend.writeRoster(account.getRoster());
2148 }
2149 }).start();
2150
2151 }
2152
2153 public List<String> getKnownHosts() {
2154 final List<String> hosts = new ArrayList<>();
2155 for (final Account account : getAccounts()) {
2156 if (!hosts.contains(account.getServer().toString())) {
2157 hosts.add(account.getServer().toString());
2158 }
2159 for (final Contact contact : account.getRoster().getContacts()) {
2160 if (contact.showInRoster()) {
2161 final String server = contact.getServer().toString();
2162 if (server != null && !hosts.contains(server)) {
2163 hosts.add(server);
2164 }
2165 }
2166 }
2167 }
2168 return hosts;
2169 }
2170
2171 public List<String> getKnownConferenceHosts() {
2172 final ArrayList<String> mucServers = new ArrayList<>();
2173 for (final Account account : accounts) {
2174 if (account.getXmppConnection() != null) {
2175 final String server = account.getXmppConnection().getMucServer();
2176 if (server != null && !mucServers.contains(server)) {
2177 mucServers.add(server);
2178 }
2179 }
2180 }
2181 return mucServers;
2182 }
2183
2184 public void sendMessagePacket(Account account, MessagePacket packet) {
2185 XmppConnection connection = account.getXmppConnection();
2186 if (connection != null) {
2187 connection.sendMessagePacket(packet);
2188 }
2189 }
2190
2191 public void sendPresencePacket(Account account, PresencePacket packet) {
2192 XmppConnection connection = account.getXmppConnection();
2193 if (connection != null) {
2194 connection.sendPresencePacket(packet);
2195 }
2196 }
2197
2198 public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
2199 final XmppConnection connection = account.getXmppConnection();
2200 if (connection != null) {
2201 connection.sendIqPacket(packet, callback);
2202 }
2203 }
2204
2205 public void sendPresence(final Account account) {
2206 sendPresencePacket(account, mPresenceGenerator.sendPresence(account));
2207 }
2208
2209 public MessageGenerator getMessageGenerator() {
2210 return this.mMessageGenerator;
2211 }
2212
2213 public PresenceGenerator getPresenceGenerator() {
2214 return this.mPresenceGenerator;
2215 }
2216
2217 public IqGenerator getIqGenerator() {
2218 return this.mIqGenerator;
2219 }
2220
2221 public IqParser getIqParser() {
2222 return this.mIqParser;
2223 }
2224
2225 public JingleConnectionManager getJingleConnectionManager() {
2226 return this.mJingleConnectionManager;
2227 }
2228
2229 public MessageArchiveService getMessageArchiveService() {
2230 return this.mMessageArchiveService;
2231 }
2232
2233 public List<Contact> findContacts(Jid jid) {
2234 ArrayList<Contact> contacts = new ArrayList<>();
2235 for (Account account : getAccounts()) {
2236 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
2237 Contact contact = account.getRoster().getContactFromRoster(jid);
2238 if (contact != null) {
2239 contacts.add(contact);
2240 }
2241 }
2242 }
2243 return contacts;
2244 }
2245
2246 public NotificationService getNotificationService() {
2247 return this.mNotificationService;
2248 }
2249
2250 public HttpConnectionManager getHttpConnectionManager() {
2251 return this.mHttpConnectionManager;
2252 }
2253
2254 public void resendFailedMessages(final Message message) {
2255 final Collection<Message> messages = new ArrayList<>();
2256 Message current = message;
2257 while (current.getStatus() == Message.STATUS_SEND_FAILED) {
2258 messages.add(current);
2259 if (current.mergeable(current.next())) {
2260 current = current.next();
2261 } else {
2262 break;
2263 }
2264 }
2265 for (final Message msg : messages) {
2266 markMessage(msg, Message.STATUS_WAITING);
2267 this.resendMessage(msg);
2268 }
2269 }
2270
2271 public void clearConversationHistory(final Conversation conversation) {
2272 conversation.clearMessages();
2273 conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam
2274 new Thread(new Runnable() {
2275 @Override
2276 public void run() {
2277 databaseBackend.deleteMessagesInConversation(conversation);
2278 }
2279 }).start();
2280 }
2281
2282 public void sendBlockRequest(final Blockable blockable) {
2283 if (blockable != null && blockable.getBlockedJid() != null) {
2284 final Jid jid = blockable.getBlockedJid();
2285 this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid), new OnIqPacketReceived() {
2286
2287 @Override
2288 public void onIqPacketReceived(final Account account, final IqPacket packet) {
2289 if (packet.getType() == IqPacket.TYPE.RESULT) {
2290 account.getBlocklist().add(jid);
2291 updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
2292 }
2293 }
2294 });
2295 }
2296 }
2297
2298 public void sendUnblockRequest(final Blockable blockable) {
2299 if (blockable != null && blockable.getJid() != null) {
2300 final Jid jid = blockable.getBlockedJid();
2301 this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() {
2302 @Override
2303 public void onIqPacketReceived(final Account account, final IqPacket packet) {
2304 if (packet.getType() == IqPacket.TYPE.RESULT) {
2305 account.getBlocklist().remove(jid);
2306 updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
2307 }
2308 }
2309 });
2310 }
2311 }
2312
2313 public interface OnMoreMessagesLoaded {
2314 public void onMoreMessagesLoaded(int count, Conversation conversation);
2315
2316 public void informUser(int r);
2317 }
2318
2319 public interface OnAccountPasswordChanged {
2320 public void onPasswordChangeSucceeded();
2321
2322 public void onPasswordChangeFailed();
2323 }
2324
2325 public interface OnAffiliationChanged {
2326 public void onAffiliationChangedSuccessful(Jid jid);
2327
2328 public void onAffiliationChangeFailed(Jid jid, int resId);
2329 }
2330
2331 public interface OnRoleChanged {
2332 public void onRoleChangedSuccessful(String nick);
2333
2334 public void onRoleChangeFailed(String nick, int resid);
2335 }
2336
2337 public interface OnConversationUpdate {
2338 public void onConversationUpdate();
2339 }
2340
2341 public interface OnAccountUpdate {
2342 public void onAccountUpdate();
2343 }
2344
2345 public interface OnRosterUpdate {
2346 public void onRosterUpdate();
2347 }
2348
2349 public interface OnMucRosterUpdate {
2350 public void onMucRosterUpdate();
2351 }
2352
2353 public interface OnConferenceOptionsPushed {
2354 public void onPushSucceeded();
2355
2356 public void onPushFailed();
2357 }
2358
2359 public class XmppConnectionBinder extends Binder {
2360 public XmppConnectionService getService() {
2361 return XmppConnectionService.this;
2362 }
2363 }
2364}