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