XmppConnectionService.java

   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.IntentFilter;
  10import android.content.SharedPreferences;
  11import android.database.ContentObserver;
  12import android.graphics.Bitmap;
  13import android.media.AudioManager;
  14import android.net.ConnectivityManager;
  15import android.net.NetworkInfo;
  16import android.net.Uri;
  17import android.os.Binder;
  18import android.os.Build;
  19import android.os.Bundle;
  20import android.os.FileObserver;
  21import android.os.IBinder;
  22import android.os.Looper;
  23import android.os.PowerManager;
  24import android.os.PowerManager.WakeLock;
  25import android.os.SystemClock;
  26import android.preference.PreferenceManager;
  27import android.provider.ContactsContract;
  28import android.security.KeyChain;
  29import android.util.DisplayMetrics;
  30import android.util.Log;
  31import android.util.LruCache;
  32import android.util.Pair;
  33
  34import net.java.otr4j.OtrException;
  35import net.java.otr4j.session.Session;
  36import net.java.otr4j.session.SessionID;
  37import net.java.otr4j.session.SessionImpl;
  38import net.java.otr4j.session.SessionStatus;
  39
  40import org.openintents.openpgp.IOpenPgpService2;
  41import org.openintents.openpgp.util.OpenPgpApi;
  42import org.openintents.openpgp.util.OpenPgpServiceConnection;
  43
  44import java.math.BigInteger;
  45import java.security.SecureRandom;
  46import java.security.cert.CertificateException;
  47import java.security.cert.X509Certificate;
  48import java.util.ArrayList;
  49import java.util.Arrays;
  50import java.util.Collection;
  51import java.util.Collections;
  52import java.util.Comparator;
  53import java.util.HashMap;
  54import java.util.Hashtable;
  55import java.util.Iterator;
  56import java.util.List;
  57import java.util.Locale;
  58import java.util.Map;
  59import java.util.concurrent.CopyOnWriteArrayList;
  60
  61import de.duenndns.ssl.MemorizingTrustManager;
  62import eu.siacs.conversations.Config;
  63import eu.siacs.conversations.R;
  64import eu.siacs.conversations.crypto.PgpEngine;
  65import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  66import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
  67import eu.siacs.conversations.entities.Account;
  68import eu.siacs.conversations.entities.Blockable;
  69import eu.siacs.conversations.entities.Bookmark;
  70import eu.siacs.conversations.entities.Contact;
  71import eu.siacs.conversations.entities.Conversation;
  72import eu.siacs.conversations.entities.Message;
  73import eu.siacs.conversations.entities.MucOptions;
  74import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
  75import eu.siacs.conversations.entities.Presence;
  76import eu.siacs.conversations.entities.PresenceTemplate;
  77import eu.siacs.conversations.entities.Roster;
  78import eu.siacs.conversations.entities.ServiceDiscoveryResult;
  79import eu.siacs.conversations.entities.Transferable;
  80import eu.siacs.conversations.entities.TransferablePlaceholder;
  81import eu.siacs.conversations.generator.IqGenerator;
  82import eu.siacs.conversations.generator.MessageGenerator;
  83import eu.siacs.conversations.generator.PresenceGenerator;
  84import eu.siacs.conversations.http.HttpConnectionManager;
  85import eu.siacs.conversations.parser.IqParser;
  86import eu.siacs.conversations.parser.MessageParser;
  87import eu.siacs.conversations.parser.PresenceParser;
  88import eu.siacs.conversations.persistance.DatabaseBackend;
  89import eu.siacs.conversations.persistance.FileBackend;
  90import eu.siacs.conversations.ui.UiCallback;
  91import eu.siacs.conversations.utils.CryptoHelper;
  92import eu.siacs.conversations.utils.ExceptionHelper;
  93import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
  94import eu.siacs.conversations.utils.PRNGFixes;
  95import eu.siacs.conversations.utils.PhoneHelper;
  96import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
  97import eu.siacs.conversations.utils.Xmlns;
  98import eu.siacs.conversations.xml.Element;
  99import eu.siacs.conversations.xmpp.OnBindListener;
 100import eu.siacs.conversations.xmpp.OnContactStatusChanged;
 101import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 102import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 103import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
 104import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
 105import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
 106import eu.siacs.conversations.xmpp.OnStatusChanged;
 107import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 108import eu.siacs.conversations.xmpp.XmppConnection;
 109import eu.siacs.conversations.xmpp.chatstate.ChatState;
 110import eu.siacs.conversations.xmpp.forms.Data;
 111import eu.siacs.conversations.xmpp.forms.Field;
 112import eu.siacs.conversations.xmpp.jid.InvalidJidException;
 113import eu.siacs.conversations.xmpp.jid.Jid;
 114import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
 115import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
 116import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 117import eu.siacs.conversations.xmpp.pep.Avatar;
 118import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 119import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 120import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 121import me.leolin.shortcutbadger.ShortcutBadger;
 122
 123public class XmppConnectionService extends Service implements OnPhoneContactsLoadedListener {
 124
 125	public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification";
 126	public static final String ACTION_DISABLE_FOREGROUND = "disable_foreground";
 127	public static final String ACTION_TRY_AGAIN = "try_again";
 128	public static final String ACTION_DISABLE_ACCOUNT = "disable_account";
 129	private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
 130	public static final String ACTION_GCM_TOKEN_REFRESH = "gcm_token_refresh";
 131	public static final String ACTION_GCM_MESSAGE_RECEIVED = "gcm_message_received";
 132	private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor();
 133	private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor();
 134	private final IBinder mBinder = new XmppConnectionBinder();
 135	private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
 136	private final IqGenerator mIqGenerator = new IqGenerator(this);
 137	private final List<String> mInProgressAvatarFetches = new ArrayList<>();
 138	public DatabaseBackend databaseBackend;
 139	private ContentObserver contactObserver = new ContentObserver(null) {
 140		@Override
 141		public void onChange(boolean selfChange) {
 142			super.onChange(selfChange);
 143			Intent intent = new Intent(getApplicationContext(),
 144					XmppConnectionService.class);
 145			intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
 146			startService(intent);
 147		}
 148	};
 149	private FileBackend fileBackend = new FileBackend(this);
 150	private MemorizingTrustManager mMemorizingTrustManager;
 151	private NotificationService mNotificationService = new NotificationService(
 152			this);
 153	private OnMessagePacketReceived mMessageParser = new MessageParser(this);
 154	private OnPresencePacketReceived mPresenceParser = new PresenceParser(this);
 155	private IqParser mIqParser = new IqParser(this);
 156	private OnIqPacketReceived mDefaultIqHandler = new OnIqPacketReceived() {
 157		@Override
 158		public void onIqPacketReceived(Account account, IqPacket packet) {
 159			if (packet.getType() != IqPacket.TYPE.RESULT) {
 160				Element error = packet.findChild("error");
 161				String text = error != null ? error.findChildContent("text") : null;
 162				if (text != null) {
 163					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": received iq error - " + text);
 164				}
 165			}
 166		}
 167	};
 168	private MessageGenerator mMessageGenerator = new MessageGenerator(this);
 169	private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
 170	private List<Account> accounts;
 171	private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
 172			this);
 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 HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
 200			this);
 201	private AvatarService mAvatarService = new AvatarService(this);
 202	private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
 203	private PushManagementService mPushManagementService = new PushManagementService(this);
 204	private OnConversationUpdate mOnConversationUpdate = null;
 205	private final FileObserver fileObserver = new FileObserver(
 206			FileBackend.getConversationsImageDirectory()) {
 207
 208		@Override
 209		public void onEvent(int event, String path) {
 210			if (event == FileObserver.DELETE) {
 211				markFileDeleted(path.split("\\.")[0]);
 212			}
 213		}
 214	};
 215	private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
 216
 217		@Override
 218		public void onJinglePacketReceived(Account account, JinglePacket packet) {
 219			mJingleConnectionManager.deliverPacket(account, packet);
 220		}
 221	};
 222	private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
 223
 224		@Override
 225		public void onMessageAcknowledged(Account account, String uuid) {
 226			for (final Conversation conversation : getConversations()) {
 227				if (conversation.getAccount() == account) {
 228					Message message = conversation.findUnsentMessageWithUuid(uuid);
 229					if (message != null) {
 230						markMessage(message, Message.STATUS_SEND);
 231					}
 232				}
 233			}
 234		}
 235	};
 236	private int convChangedListenerCount = 0;
 237	private OnShowErrorToast mOnShowErrorToast = null;
 238	private int showErrorToastListenerCount = 0;
 239	private int unreadCount = -1;
 240	private OnAccountUpdate mOnAccountUpdate = null;
 241	private OnCaptchaRequested mOnCaptchaRequested = null;
 242	private int accountChangedListenerCount = 0;
 243	private int captchaRequestedListenerCount = 0;
 244	private OnRosterUpdate mOnRosterUpdate = null;
 245	private OnUpdateBlocklist mOnUpdateBlocklist = null;
 246	private int updateBlocklistListenerCount = 0;
 247	private int rosterChangedListenerCount = 0;
 248	private OnMucRosterUpdate mOnMucRosterUpdate = null;
 249	private int mucRosterChangedListenerCount = 0;
 250	private OnKeyStatusUpdated mOnKeyStatusUpdated = null;
 251	private int keyStatusUpdatedListenerCount = 0;
 252	private SecureRandom mRandom;
 253	private LruCache<Pair<String,String>,ServiceDiscoveryResult> discoCache = new LruCache<>(20);
 254	private final OnBindListener mOnBindListener = new OnBindListener() {
 255
 256		@Override
 257		public void onBind(final Account account) {
 258			synchronized (mInProgressAvatarFetches) {
 259				for (Iterator<String> iterator = mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) {
 260					final String KEY = iterator.next();
 261					if (KEY.startsWith(account.getJid().toBareJid() + "_")) {
 262						iterator.remove();
 263					}
 264				}
 265			}
 266			account.getRoster().clearPresences();
 267			mJingleConnectionManager.cancelInTransmission();
 268			fetchRosterFromServer(account);
 269			fetchBookmarks(account);
 270			sendPresence(account);
 271			if (mPushManagementService.available(account)) {
 272				mPushManagementService.registerPushTokenOnServer(account);
 273			}
 274			connectMultiModeConversations(account);
 275			syncDirtyContacts(account);
 276		}
 277	};
 278	private OnStatusChanged statusListener = new OnStatusChanged() {
 279
 280		@Override
 281		public void onStatusChanged(final Account account) {
 282			XmppConnection connection = account.getXmppConnection();
 283			if (mOnAccountUpdate != null) {
 284				mOnAccountUpdate.onAccountUpdate();
 285			}
 286			if (account.getStatus() == Account.State.ONLINE) {
 287				mMessageArchiveService.executePendingQueries(account);
 288				if (connection != null && connection.getFeatures().csi()) {
 289					if (checkListeners()) {
 290						Log.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//inactive");
 291						connection.sendInactive();
 292					} else {
 293						Log.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//active");
 294						connection.sendActive();
 295					}
 296				}
 297				List<Conversation> conversations = getConversations();
 298				for (Conversation conversation : conversations) {
 299					if (conversation.getAccount() == account
 300							&& !account.pendingConferenceJoins.contains(conversation)) {
 301						if (!conversation.startOtrIfNeeded()) {
 302							Log.d(Config.LOGTAG,account.getJid().toBareJid()+": couldn't start OTR with "+conversation.getContact().getJid()+" when needed");
 303						}
 304						sendUnsentMessages(conversation);
 305					}
 306				}
 307				for (Conversation conversation : account.pendingConferenceLeaves) {
 308					leaveMuc(conversation);
 309				}
 310				account.pendingConferenceLeaves.clear();
 311				for (Conversation conversation : account.pendingConferenceJoins) {
 312					joinMuc(conversation);
 313				}
 314				account.pendingConferenceJoins.clear();
 315				scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
 316			} else if (account.getStatus() == Account.State.OFFLINE) {
 317				resetSendingToWaiting(account);
 318				final boolean disabled = account.isOptionSet(Account.OPTION_DISABLED);
 319				final boolean pushMode = Config.CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND
 320						&& mPushManagementService.available(account)
 321						&& checkListeners();
 322				Log.d(Config.LOGTAG,account.getJid().toBareJid()+": push mode "+Boolean.toString(pushMode));
 323				if (!disabled && !pushMode) {
 324					int timeToReconnect = mRandom.nextInt(20) + 10;
 325					scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
 326				}
 327			} else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
 328				databaseBackend.updateAccount(account);
 329				reconnectAccount(account, true, false);
 330			} else if ((account.getStatus() != Account.State.CONNECTING)
 331					&& (account.getStatus() != Account.State.NO_INTERNET)) {
 332				if (connection != null) {
 333					int next = connection.getTimeToNextAttempt();
 334					Log.d(Config.LOGTAG, account.getJid().toBareJid()
 335							+ ": error connecting account. try again in "
 336							+ next + "s for the "
 337							+ (connection.getAttempt() + 1) + " time");
 338					scheduleWakeUpCall(next, account.getUuid().hashCode());
 339				}
 340			}
 341			getNotificationService().updateErrorNotification();
 342		}
 343	};
 344	private OpenPgpServiceConnection pgpServiceConnection;
 345	private PgpEngine mPgpEngine = null;
 346	private WakeLock wakeLock;
 347	private PowerManager pm;
 348	private LruCache<String, Bitmap> mBitmapCache;
 349	private Thread mPhoneContactMergerThread;
 350	private EventReceiver mEventReceiver = new EventReceiver();
 351
 352	private boolean mRestoredFromDatabase = false;
 353
 354	private static String generateFetchKey(Account account, final Avatar avatar) {
 355		return account.getJid().toBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum;
 356	}
 357
 358	public boolean areMessagesInitialized() {
 359		return this.mRestoredFromDatabase;
 360	}
 361
 362	public PgpEngine getPgpEngine() {
 363		if (!Config.supportOpenPgp()) {
 364			return null;
 365		} else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
 366			if (this.mPgpEngine == null) {
 367				this.mPgpEngine = new PgpEngine(new OpenPgpApi(
 368						getApplicationContext(),
 369						pgpServiceConnection.getService()), this);
 370			}
 371			return mPgpEngine;
 372		} else {
 373			return null;
 374		}
 375
 376	}
 377
 378	public FileBackend getFileBackend() {
 379		return this.fileBackend;
 380	}
 381
 382	public AvatarService getAvatarService() {
 383		return this.mAvatarService;
 384	}
 385
 386	public void attachLocationToConversation(final Conversation conversation,
 387											 final Uri uri,
 388											 final UiCallback<Message> callback) {
 389		int encryption = conversation.getNextEncryption();
 390		if (encryption == Message.ENCRYPTION_PGP) {
 391			encryption = Message.ENCRYPTION_DECRYPTED;
 392		}
 393		Message message = new Message(conversation, uri.toString(), encryption);
 394		if (conversation.getNextCounterpart() != null) {
 395			message.setCounterpart(conversation.getNextCounterpart());
 396		}
 397		if (encryption == Message.ENCRYPTION_DECRYPTED) {
 398			getPgpEngine().encrypt(message, callback);
 399		} else {
 400			callback.success(message);
 401		}
 402	}
 403
 404	public void attachFileToConversation(final Conversation conversation,
 405										 final Uri uri,
 406										 final UiCallback<Message> callback) {
 407		if (FileBackend.weOwnFile(this, uri)) {
 408			Log.d(Config.LOGTAG,"trying to attach file that belonged to us");
 409			callback.error(R.string.security_error_invalid_file_access, null);
 410			return;
 411		}
 412		final Message message;
 413		if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 414			message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
 415		} else {
 416			message = new Message(conversation, "", conversation.getNextEncryption());
 417		}
 418		message.setCounterpart(conversation.getNextCounterpart());
 419		message.setType(Message.TYPE_FILE);
 420		String path = getFileBackend().getOriginalPath(uri);
 421		if (path != null) {
 422			message.setRelativeFilePath(path);
 423			getFileBackend().updateFileParams(message);
 424			if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 425				getPgpEngine().encrypt(message, callback);
 426			} else {
 427				callback.success(message);
 428			}
 429		} else {
 430			mFileAddingExecutor.execute(new Runnable() {
 431				@Override
 432				public void run() {
 433					try {
 434						getFileBackend().copyFileToPrivateStorage(message, uri);
 435						getFileBackend().updateFileParams(message);
 436						if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 437							getPgpEngine().encrypt(message, callback);
 438						} else {
 439							callback.success(message);
 440						}
 441					} catch (FileBackend.FileCopyException e) {
 442						callback.error(e.getResId(), message);
 443					}
 444				}
 445			});
 446		}
 447	}
 448
 449	public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
 450		if (FileBackend.weOwnFile(this, uri)) {
 451			Log.d(Config.LOGTAG,"trying to attach file that belonged to us");
 452			callback.error(R.string.security_error_invalid_file_access, null);
 453			return;
 454		}
 455		final String compressPictures = getCompressPicturesPreference();
 456		if ("never".equals(compressPictures)
 457				|| ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))) {
 458			Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+ ": not compressing picture. sending as file");
 459			attachFileToConversation(conversation, uri, callback);
 460			return;
 461		}
 462		final Message message;
 463		if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 464			message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
 465		} else {
 466			message = new Message(conversation, "", conversation.getNextEncryption());
 467		}
 468		message.setCounterpart(conversation.getNextCounterpart());
 469		message.setType(Message.TYPE_IMAGE);
 470		mFileAddingExecutor.execute(new Runnable() {
 471
 472			@Override
 473			public void run() {
 474				try {
 475					getFileBackend().copyImageToPrivateStorage(message, uri);
 476					if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 477						getPgpEngine().encrypt(message, callback);
 478					} else {
 479						callback.success(message);
 480					}
 481				} catch (final FileBackend.FileCopyException e) {
 482					callback.error(e.getResId(), message);
 483				}
 484			}
 485		});
 486	}
 487
 488	public Conversation find(Bookmark bookmark) {
 489		return find(bookmark.getAccount(), bookmark.getJid());
 490	}
 491
 492	public Conversation find(final Account account, final Jid jid) {
 493		return find(getConversations(), account, jid);
 494	}
 495
 496	@Override
 497	public int onStartCommand(Intent intent, int flags, int startId) {
 498		final String action = intent == null ? null : intent.getAction();
 499		boolean interactive = false;
 500		if (action != null) {
 501			switch (action) {
 502				case ConnectivityManager.CONNECTIVITY_ACTION:
 503					if (hasInternetConnection() && Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
 504						resetAllAttemptCounts(true);
 505					}
 506					break;
 507				case ACTION_MERGE_PHONE_CONTACTS:
 508					if (mRestoredFromDatabase) {
 509						loadPhoneContacts();
 510					}
 511					return START_STICKY;
 512				case Intent.ACTION_SHUTDOWN:
 513					logoutAndSave(true);
 514					return START_NOT_STICKY;
 515				case ACTION_CLEAR_NOTIFICATION:
 516					mNotificationService.clear();
 517					break;
 518				case ACTION_DISABLE_FOREGROUND:
 519					getPreferences().edit().putBoolean("keep_foreground_service", false).commit();
 520					toggleForegroundService();
 521					break;
 522				case ACTION_TRY_AGAIN:
 523					resetAllAttemptCounts(false);
 524					interactive = true;
 525					break;
 526				case ACTION_DISABLE_ACCOUNT:
 527					try {
 528						String jid = intent.getStringExtra("account");
 529						Account account = jid == null ? null : findAccountByJid(Jid.fromString(jid));
 530						if (account != null) {
 531							account.setOption(Account.OPTION_DISABLED, true);
 532							updateAccount(account);
 533						}
 534					} catch (final InvalidJidException ignored) {
 535						break;
 536					}
 537					break;
 538				case AudioManager.RINGER_MODE_CHANGED_ACTION:
 539					if (xaOnSilentMode()) {
 540						refreshAllPresences();
 541					}
 542					break;
 543				case Intent.ACTION_SCREEN_OFF:
 544				case Intent.ACTION_SCREEN_ON:
 545					if (awayWhenScreenOff()) {
 546						refreshAllPresences();
 547					}
 548					break;
 549				case ACTION_GCM_TOKEN_REFRESH:
 550					refreshAllGcmTokens();
 551					break;
 552				case ACTION_GCM_MESSAGE_RECEIVED:
 553					Log.d(Config.LOGTAG,"gcm push message arrived in service. extras="+intent.getExtras());
 554			}
 555		}
 556		this.wakeLock.acquire();
 557
 558		for (Account account : accounts) {
 559			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 560				if (!hasInternetConnection()) {
 561					account.setStatus(Account.State.NO_INTERNET);
 562					if (statusListener != null) {
 563						statusListener.onStatusChanged(account);
 564					}
 565				} else {
 566					if (account.getStatus() == Account.State.NO_INTERNET) {
 567						account.setStatus(Account.State.OFFLINE);
 568						if (statusListener != null) {
 569							statusListener.onStatusChanged(account);
 570						}
 571					}
 572					if (account.getStatus() == Account.State.ONLINE) {
 573						long lastReceived = account.getXmppConnection().getLastPacketReceived();
 574						long lastSent = account.getXmppConnection().getLastPingSent();
 575						long pingInterval = "ui".equals(action) ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
 576						long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime();
 577						long pingTimeoutIn = (lastSent + Config.PING_TIMEOUT * 1000) - SystemClock.elapsedRealtime();
 578						if (lastSent > lastReceived) {
 579							if (pingTimeoutIn < 0) {
 580								Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ping timeout");
 581								this.reconnectAccount(account, true, interactive);
 582							} else {
 583								int secs = (int) (pingTimeoutIn / 1000);
 584								this.scheduleWakeUpCall(secs, account.getUuid().hashCode());
 585							}
 586						} else if (msToNextPing <= 0) {
 587							account.getXmppConnection().sendPing();
 588							Log.d(Config.LOGTAG, account.getJid().toBareJid() + " send ping");
 589							this.scheduleWakeUpCall(Config.PING_TIMEOUT, account.getUuid().hashCode());
 590						} else {
 591							this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
 592						}
 593					} else if (account.getStatus() == Account.State.OFFLINE) {
 594						reconnectAccount(account, true, interactive);
 595					} else if (account.getStatus() == Account.State.CONNECTING) {
 596						long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
 597						long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
 598						long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
 599						long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
 600						if (timeout < 0) {
 601							Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting");
 602							reconnectAccount(account, true, interactive);
 603						} else if (discoTimeout < 0) {
 604							account.getXmppConnection().sendDiscoTimeout();
 605							scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
 606						} else {
 607							scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
 608						}
 609					} else {
 610						if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
 611							reconnectAccount(account, true, interactive);
 612						}
 613					}
 614				}
 615				if (mOnAccountUpdate != null) {
 616					mOnAccountUpdate.onAccountUpdate();
 617				}
 618			}
 619		}
 620		if (wakeLock.isHeld()) {
 621			try {
 622				wakeLock.release();
 623			} catch (final RuntimeException ignored) {
 624			}
 625		}
 626		return START_STICKY;
 627	}
 628
 629	private boolean xaOnSilentMode() {
 630		return getPreferences().getBoolean("xa_on_silent_mode", false);
 631	}
 632
 633	private boolean manuallyChangePresence() {
 634		return getPreferences().getBoolean("manually_change_presence", false);
 635	}
 636
 637	private boolean treatVibrateAsSilent() {
 638		return getPreferences().getBoolean("treat_vibrate_as_silent", false);
 639	}
 640
 641	private boolean awayWhenScreenOff() {
 642		return getPreferences().getBoolean("away_when_screen_off", false);
 643	}
 644
 645	private String getCompressPicturesPreference() {
 646		return getPreferences().getString("picture_compression", "auto");
 647	}
 648
 649	private Presence.Status getTargetPresence() {
 650		if (xaOnSilentMode() && isPhoneSilenced()) {
 651			return Presence.Status.XA;
 652		} else if (awayWhenScreenOff() && !isInteractive()) {
 653			return Presence.Status.AWAY;
 654		} else {
 655			return Presence.Status.ONLINE;
 656		}
 657	}
 658
 659	@SuppressLint("NewApi")
 660	@SuppressWarnings("deprecation")
 661	public boolean isInteractive() {
 662		final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 663
 664		final boolean isScreenOn;
 665		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
 666			isScreenOn = pm.isScreenOn();
 667		} else {
 668			isScreenOn = pm.isInteractive();
 669		}
 670		return isScreenOn;
 671	}
 672
 673	private boolean isPhoneSilenced() {
 674		AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
 675		if (treatVibrateAsSilent()) {
 676			return audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
 677		} else {
 678			return audioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT;
 679		}
 680	}
 681
 682	private void resetAllAttemptCounts(boolean reallyAll) {
 683		Log.d(Config.LOGTAG, "resetting all attempt counts");
 684		for (Account account : accounts) {
 685			if (account.hasErrorStatus() || reallyAll) {
 686				final XmppConnection connection = account.getXmppConnection();
 687				if (connection != null) {
 688					connection.resetAttemptCount();
 689				}
 690			}
 691		}
 692	}
 693
 694	public boolean hasInternetConnection() {
 695		ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
 696				.getSystemService(Context.CONNECTIVITY_SERVICE);
 697		NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
 698		return activeNetwork != null && activeNetwork.isConnected();
 699	}
 700
 701	@SuppressLint("TrulyRandom")
 702	@Override
 703	public void onCreate() {
 704		ExceptionHelper.init(getApplicationContext());
 705		PRNGFixes.apply();
 706		this.mRandom = new SecureRandom();
 707		updateMemorizingTrustmanager();
 708		final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
 709		final int cacheSize = maxMemory / 8;
 710		this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
 711			@Override
 712			protected int sizeOf(final String key, final Bitmap bitmap) {
 713				return bitmap.getByteCount() / 1024;
 714			}
 715		};
 716
 717		this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
 718		this.accounts = databaseBackend.getAccounts();
 719
 720		restoreFromDatabase();
 721
 722		getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
 723		this.fileObserver.startWatching();
 724
 725		if (Config.supportOpenPgp()) {
 726			this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() {
 727				@Override
 728				public void onBound(IOpenPgpService2 service) {
 729					for (Account account : accounts) {
 730						if (account.getPgpDecryptionService() != null) {
 731							account.getPgpDecryptionService().onOpenPgpServiceBound();
 732						}
 733					}
 734				}
 735
 736				@Override
 737				public void onError(Exception e) {
 738				}
 739			});
 740			this.pgpServiceConnection.bindToService();
 741		}
 742
 743		this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 744		this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "XmppConnectionService");
 745		toggleForegroundService();
 746		updateUnreadCountBadge();
 747		toggleScreenEventReceiver();
 748	}
 749
 750	@Override
 751	public void onTrimMemory(int level) {
 752		super.onTrimMemory(level);
 753		if (level >= TRIM_MEMORY_COMPLETE) {
 754			Log.d(Config.LOGTAG, "clear cache due to low memory");
 755			getBitmapCache().evictAll();
 756		}
 757	}
 758
 759	@Override
 760	public void onDestroy() {
 761		try {
 762			unregisterReceiver(this.mEventReceiver);
 763		} catch (IllegalArgumentException e) {
 764			//ignored
 765		}
 766		super.onDestroy();
 767	}
 768
 769	public void toggleScreenEventReceiver() {
 770		if (awayWhenScreenOff() && !manuallyChangePresence()) {
 771			final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
 772			filter.addAction(Intent.ACTION_SCREEN_OFF);
 773			registerReceiver(this.mEventReceiver, filter);
 774		} else {
 775			try {
 776				unregisterReceiver(this.mEventReceiver);
 777			} catch (IllegalArgumentException e) {
 778				//ignored
 779			}
 780		}
 781	}
 782
 783	public void toggleForegroundService() {
 784		if (getPreferences().getBoolean("keep_foreground_service", false)) {
 785			startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
 786		} else {
 787			stopForeground(true);
 788		}
 789	}
 790
 791	@Override
 792	public void onTaskRemoved(final Intent rootIntent) {
 793		super.onTaskRemoved(rootIntent);
 794		if (!getPreferences().getBoolean("keep_foreground_service", false)) {
 795			this.logoutAndSave(false);
 796		}
 797	}
 798
 799	private void logoutAndSave(boolean stop) {
 800		int activeAccounts = 0;
 801		for (final Account account : accounts) {
 802			if (account.getStatus() != Account.State.DISABLED) {
 803				activeAccounts++;
 804			}
 805			databaseBackend.writeRoster(account.getRoster());
 806			if (account.getXmppConnection() != null) {
 807				new Thread(new Runnable() {
 808					@Override
 809					public void run() {
 810						disconnect(account, false);
 811					}
 812				}).start();
 813			}
 814		}
 815		if (stop || activeAccounts == 0) {
 816			Log.d(Config.LOGTAG, "good bye");
 817			stopSelf();
 818		}
 819	}
 820
 821	private void cancelWakeUpCall(int requestCode) {
 822		final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
 823		final Intent intent = new Intent(this, EventReceiver.class);
 824		intent.setAction("ping");
 825		alarmManager.cancel(PendingIntent.getBroadcast(this, requestCode, intent, 0));
 826	}
 827
 828	public void scheduleWakeUpCall(int seconds, int requestCode) {
 829		final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000;
 830
 831		Context context = getApplicationContext();
 832		AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
 833
 834		Intent intent = new Intent(context, EventReceiver.class);
 835		intent.setAction("ping");
 836		PendingIntent alarmIntent = PendingIntent.getBroadcast(context, requestCode, intent, 0);
 837		alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, alarmIntent);
 838	}
 839
 840	public XmppConnection createConnection(final Account account) {
 841		final SharedPreferences sharedPref = getPreferences();
 842		account.setResource(sharedPref.getString("resource", "mobile")
 843				.toLowerCase(Locale.getDefault()));
 844		final XmppConnection connection = new XmppConnection(account, this);
 845		connection.setOnMessagePacketReceivedListener(this.mMessageParser);
 846		connection.setOnStatusChangedListener(this.statusListener);
 847		connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
 848		connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
 849		connection.setOnJinglePacketReceivedListener(this.jingleListener);
 850		connection.setOnBindListener(this.mOnBindListener);
 851		connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
 852		connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
 853		connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService);
 854		AxolotlService axolotlService = account.getAxolotlService();
 855		if (axolotlService != null) {
 856			connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
 857		}
 858		return connection;
 859	}
 860
 861	public void sendChatState(Conversation conversation) {
 862		if (sendChatStates()) {
 863			MessagePacket packet = mMessageGenerator.generateChatState(conversation);
 864			sendMessagePacket(conversation.getAccount(), packet);
 865		}
 866	}
 867
 868	private void sendFileMessage(final Message message, final boolean delay) {
 869		Log.d(Config.LOGTAG, "send file message");
 870		final Account account = message.getConversation().getAccount();
 871		if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize())) {
 872			mHttpConnectionManager.createNewUploadConnection(message, delay);
 873		} else {
 874			mJingleConnectionManager.createNewConnection(message);
 875		}
 876	}
 877
 878	public void sendMessage(final Message message) {
 879		sendMessage(message, false, false);
 880	}
 881
 882	private void sendMessage(final Message message, final boolean resend, final boolean delay) {
 883		final Account account = message.getConversation().getAccount();
 884		final Conversation conversation = message.getConversation();
 885		account.deactivateGracePeriod();
 886		MessagePacket packet = null;
 887		final boolean addToConversation = (conversation.getMode() != Conversation.MODE_MULTI
 888				|| account.getServerIdentity() != XmppConnection.Identity.SLACK)
 889				&& !message.edited();
 890		boolean saveInDb = addToConversation;
 891		message.setStatus(Message.STATUS_WAITING);
 892
 893		if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) {
 894			message.getConversation().endOtrIfNeeded();
 895			message.getConversation().findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR,
 896					new Conversation.OnMessageFound() {
 897						@Override
 898						public void onMessageFound(Message message) {
 899							markMessage(message, Message.STATUS_SEND_FAILED);
 900						}
 901					});
 902		}
 903
 904		if (account.isOnlineAndConnected()) {
 905			switch (message.getEncryption()) {
 906				case Message.ENCRYPTION_NONE:
 907					if (message.needsUploading()) {
 908						if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize())
 909								|| message.fixCounterpart()) {
 910							this.sendFileMessage(message, delay);
 911						} else {
 912							break;
 913						}
 914					} else {
 915						packet = mMessageGenerator.generateChat(message);
 916					}
 917					break;
 918				case Message.ENCRYPTION_PGP:
 919				case Message.ENCRYPTION_DECRYPTED:
 920					if (message.needsUploading()) {
 921						if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize())
 922								|| message.fixCounterpart()) {
 923							this.sendFileMessage(message, delay);
 924						} else {
 925							break;
 926						}
 927					} else {
 928						packet = mMessageGenerator.generatePgpChat(message);
 929					}
 930					break;
 931				case Message.ENCRYPTION_OTR:
 932					SessionImpl otrSession = conversation.getOtrSession();
 933					if (otrSession != null && otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
 934						try {
 935							message.setCounterpart(Jid.fromSessionID(otrSession.getSessionID()));
 936						} catch (InvalidJidException e) {
 937							break;
 938						}
 939						if (message.needsUploading()) {
 940							mJingleConnectionManager.createNewConnection(message);
 941						} else {
 942							packet = mMessageGenerator.generateOtrChat(message);
 943						}
 944					} else if (otrSession == null) {
 945						if (message.fixCounterpart()) {
 946							conversation.startOtrSession(message.getCounterpart().getResourcepart(), true);
 947						} else {
 948							Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not fix counterpart for OTR message to contact "+message.getContact().getJid());
 949							break;
 950						}
 951					} else {
 952						Log.d(Config.LOGTAG,account.getJid().toBareJid()+" OTR session with "+message.getContact()+" is in wrong state: "+otrSession.getSessionStatus().toString());
 953					}
 954					break;
 955				case Message.ENCRYPTION_AXOLOTL:
 956					message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
 957					if (message.needsUploading()) {
 958						if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize())
 959								|| message.fixCounterpart()) {
 960							this.sendFileMessage(message, delay);
 961						} else {
 962							break;
 963						}
 964					} else {
 965						XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message);
 966						if (axolotlMessage == null) {
 967							account.getAxolotlService().preparePayloadMessage(message, delay);
 968						} else {
 969							packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
 970						}
 971					}
 972					break;
 973
 974			}
 975			if (packet != null) {
 976				if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) {
 977					message.setStatus(Message.STATUS_UNSEND);
 978				} else {
 979					message.setStatus(Message.STATUS_SEND);
 980				}
 981			}
 982		} else {
 983			switch (message.getEncryption()) {
 984				case Message.ENCRYPTION_DECRYPTED:
 985					if (!message.needsUploading()) {
 986						String pgpBody = message.getEncryptedBody();
 987						String decryptedBody = message.getBody();
 988						message.setBody(pgpBody);
 989						message.setEncryption(Message.ENCRYPTION_PGP);
 990						databaseBackend.createMessage(message);
 991						saveInDb = false;
 992						message.setBody(decryptedBody);
 993						message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 994					}
 995					break;
 996				case Message.ENCRYPTION_OTR:
 997					if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) {
 998						Log.d(Config.LOGTAG,account.getJid().toBareJid()+": create otr session without starting for "+message.getContact().getJid());
 999						conversation.startOtrSession(message.getCounterpart().getResourcepart(), false);
1000					}
1001					break;
1002				case Message.ENCRYPTION_AXOLOTL:
1003					message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
1004					break;
1005			}
1006		}
1007
1008		if (resend) {
1009			if (packet != null && addToConversation) {
1010				if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) {
1011					markMessage(message, Message.STATUS_UNSEND);
1012				} else {
1013					markMessage(message, Message.STATUS_SEND);
1014				}
1015			}
1016		} else {
1017			if (addToConversation) {
1018				conversation.add(message);
1019			}
1020			if (message.getEncryption() == Message.ENCRYPTION_NONE || saveEncryptedMessages()) {
1021				if (saveInDb) {
1022					databaseBackend.createMessage(message);
1023				} else if (message.edited()) {
1024					databaseBackend.updateMessage(message, message.getEditedId());
1025				}
1026			}
1027			updateConversationUi();
1028		}
1029		if (packet != null) {
1030			if (delay) {
1031				mMessageGenerator.addDelay(packet, message.getTimeSent());
1032			}
1033			if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
1034				if (this.sendChatStates()) {
1035					packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
1036				}
1037			}
1038			sendMessagePacket(account, packet);
1039		}
1040	}
1041
1042	private void sendUnsentMessages(final Conversation conversation) {
1043		conversation.findWaitingMessages(new Conversation.OnMessageFound() {
1044
1045			@Override
1046			public void onMessageFound(Message message) {
1047				resendMessage(message, true);
1048			}
1049		});
1050	}
1051
1052	public void resendMessage(final Message message, final boolean delay) {
1053		sendMessage(message, true, delay);
1054	}
1055
1056	public void fetchRosterFromServer(final Account account) {
1057		final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
1058		if (!"".equals(account.getRosterVersion())) {
1059			Log.d(Config.LOGTAG, account.getJid().toBareJid()
1060					+ ": fetching roster version " + account.getRosterVersion());
1061		} else {
1062			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
1063		}
1064		iqPacket.query(Xmlns.ROSTER).setAttribute("ver", account.getRosterVersion());
1065		sendIqPacket(account, iqPacket, mIqParser);
1066	}
1067
1068	public void fetchBookmarks(final Account account) {
1069		final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
1070		final Element query = iqPacket.query("jabber:iq:private");
1071		query.addChild("storage", "storage:bookmarks");
1072		final OnIqPacketReceived callback = new OnIqPacketReceived() {
1073
1074			@Override
1075			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1076				if (packet.getType() == IqPacket.TYPE.RESULT) {
1077					final Element query = packet.query();
1078					final HashMap<Jid, Bookmark> bookmarks = new HashMap<>();
1079					final Element storage = query.findChild("storage", "storage:bookmarks");
1080					final boolean autojoin = respectAutojoin();
1081					if (storage != null) {
1082						for (final Element item : storage.getChildren()) {
1083							if (item.getName().equals("conference")) {
1084								final Bookmark bookmark = Bookmark.parse(item, account);
1085								Bookmark old = bookmarks.put(bookmark.getJid(), bookmark);
1086								if (old != null && old.getBookmarkName() != null && bookmark.getBookmarkName() == null) {
1087									bookmark.setBookmarkName(old.getBookmarkName());
1088								}
1089								Conversation conversation = find(bookmark);
1090								if (conversation != null) {
1091									conversation.setBookmark(bookmark);
1092								} else if (bookmark.autojoin() && bookmark.getJid() != null && autojoin) {
1093									conversation = findOrCreateConversation(
1094											account, bookmark.getJid(), true);
1095									conversation.setBookmark(bookmark);
1096									joinMuc(conversation);
1097								}
1098							}
1099						}
1100					}
1101					account.setBookmarks(new ArrayList<>(bookmarks.values()));
1102				} else {
1103					Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not fetch bookmarks");
1104				}
1105			}
1106		};
1107		sendIqPacket(account, iqPacket, callback);
1108	}
1109
1110	public void pushBookmarks(Account account) {
1111		Log.d(Config.LOGTAG, account.getJid().toBareJid()+": pushing bookmarks");
1112		IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
1113		Element query = iqPacket.query("jabber:iq:private");
1114		Element storage = query.addChild("storage", "storage:bookmarks");
1115		for (Bookmark bookmark : account.getBookmarks()) {
1116			storage.addChild(bookmark);
1117		}
1118		sendIqPacket(account, iqPacket, mDefaultIqHandler);
1119	}
1120
1121	public void onPhoneContactsLoaded(final List<Bundle> phoneContacts) {
1122		if (mPhoneContactMergerThread != null) {
1123			mPhoneContactMergerThread.interrupt();
1124		}
1125		mPhoneContactMergerThread = new Thread(new Runnable() {
1126			@Override
1127			public void run() {
1128				Log.d(Config.LOGTAG, "start merging phone contacts with roster");
1129				for (Account account : accounts) {
1130					List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts();
1131					for (Bundle phoneContact : phoneContacts) {
1132						if (Thread.interrupted()) {
1133							Log.d(Config.LOGTAG, "interrupted merging phone contacts");
1134							return;
1135						}
1136						Jid jid;
1137						try {
1138							jid = Jid.fromString(phoneContact.getString("jid"));
1139						} catch (final InvalidJidException e) {
1140							continue;
1141						}
1142						final Contact contact = account.getRoster().getContact(jid);
1143						String systemAccount = phoneContact.getInt("phoneid")
1144								+ "#"
1145								+ phoneContact.getString("lookup");
1146						contact.setSystemAccount(systemAccount);
1147						if (contact.setPhotoUri(phoneContact.getString("photouri"))) {
1148							getAvatarService().clear(contact);
1149						}
1150						contact.setSystemName(phoneContact.getString("displayname"));
1151						withSystemAccounts.remove(contact);
1152					}
1153					for (Contact contact : withSystemAccounts) {
1154						contact.setSystemAccount(null);
1155						contact.setSystemName(null);
1156						if (contact.setPhotoUri(null)) {
1157							getAvatarService().clear(contact);
1158						}
1159					}
1160				}
1161				Log.d(Config.LOGTAG, "finished merging phone contacts");
1162				updateAccountUi();
1163			}
1164		});
1165		mPhoneContactMergerThread.start();
1166	}
1167
1168	private void restoreFromDatabase() {
1169		synchronized (this.conversations) {
1170			final Map<String, Account> accountLookupTable = new Hashtable<>();
1171			for (Account account : this.accounts) {
1172				accountLookupTable.put(account.getUuid(), account);
1173			}
1174			this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
1175			for (Conversation conversation : this.conversations) {
1176				Account account = accountLookupTable.get(conversation.getAccountUuid());
1177				conversation.setAccount(account);
1178			}
1179			Runnable runnable = new Runnable() {
1180				@Override
1181				public void run() {
1182					Log.d(Config.LOGTAG, "restoring roster");
1183					for (Account account : accounts) {
1184						databaseBackend.readRoster(account.getRoster());
1185						account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage
1186					}
1187					getBitmapCache().evictAll();
1188					Looper.prepare();
1189					loadPhoneContacts();
1190					Log.d(Config.LOGTAG, "restoring messages");
1191					for (Conversation conversation : conversations) {
1192						conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
1193						checkDeletedFiles(conversation);
1194						conversation.findUnreadMessages(new Conversation.OnMessageFound() {
1195							@Override
1196							public void onMessageFound(Message message) {
1197								mNotificationService.pushFromBacklog(message);
1198							}
1199						});
1200					}
1201					mNotificationService.finishBacklog(false);
1202					mRestoredFromDatabase = true;
1203					Log.d(Config.LOGTAG, "restored all messages");
1204					updateConversationUi();
1205				}
1206			};
1207			mDatabaseExecutor.execute(runnable);
1208		}
1209	}
1210
1211	public void loadPhoneContacts() {
1212		PhoneHelper.loadPhoneContacts(getApplicationContext(),
1213				new CopyOnWriteArrayList<Bundle>(),
1214				XmppConnectionService.this);
1215	}
1216
1217	public List<Conversation> getConversations() {
1218		return this.conversations;
1219	}
1220
1221	private void checkDeletedFiles(Conversation conversation) {
1222		conversation.findMessagesWithFiles(new Conversation.OnMessageFound() {
1223
1224			@Override
1225			public void onMessageFound(Message message) {
1226				if (!getFileBackend().isFileAvailable(message)) {
1227					message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
1228					final int s = message.getStatus();
1229					if (s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) {
1230						markMessage(message, Message.STATUS_SEND_FAILED);
1231					}
1232				}
1233			}
1234		});
1235	}
1236
1237	private void markFileDeleted(String uuid) {
1238		for (Conversation conversation : getConversations()) {
1239			Message message = conversation.findMessageWithFileAndUuid(uuid);
1240			if (message != null) {
1241				if (!getFileBackend().isFileAvailable(message)) {
1242					message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
1243					final int s = message.getStatus();
1244					if (s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) {
1245						markMessage(message, Message.STATUS_SEND_FAILED);
1246					} else {
1247						updateConversationUi();
1248					}
1249				}
1250				return;
1251			}
1252		}
1253	}
1254
1255	public void populateWithOrderedConversations(final List<Conversation> list) {
1256		populateWithOrderedConversations(list, true);
1257	}
1258
1259	public void populateWithOrderedConversations(final List<Conversation> list, boolean includeNoFileUpload) {
1260		list.clear();
1261		if (includeNoFileUpload) {
1262			list.addAll(getConversations());
1263		} else {
1264			for (Conversation conversation : getConversations()) {
1265				if (conversation.getMode() == Conversation.MODE_SINGLE
1266						|| conversation.getAccount().httpUploadAvailable()) {
1267					list.add(conversation);
1268				}
1269			}
1270		}
1271		Collections.sort(list, new Comparator<Conversation>() {
1272			@Override
1273			public int compare(Conversation lhs, Conversation rhs) {
1274				Message left = lhs.getLatestMessage();
1275				Message right = rhs.getLatestMessage();
1276				if (left.getTimeSent() > right.getTimeSent()) {
1277					return -1;
1278				} else if (left.getTimeSent() < right.getTimeSent()) {
1279					return 1;
1280				} else {
1281					return 0;
1282				}
1283			}
1284		});
1285	}
1286
1287	public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
1288		if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback)) {
1289			return;
1290		} else if (timestamp == 0) {
1291			return;
1292		}
1293		Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp));
1294		Runnable runnable = new Runnable() {
1295			@Override
1296			public void run() {
1297				final Account account = conversation.getAccount();
1298				List<Message> messages = databaseBackend.getMessages(conversation, 50, timestamp);
1299				if (messages.size() > 0) {
1300					conversation.addAll(0, messages);
1301					checkDeletedFiles(conversation);
1302					callback.onMoreMessagesLoaded(messages.size(), conversation);
1303				} else if (conversation.hasMessagesLeftOnServer()
1304						&& account.isOnlineAndConnected()
1305						&& conversation.getLastClearHistory() == 0) {
1306					if ((conversation.getMode() == Conversation.MODE_SINGLE && account.getXmppConnection().getFeatures().mam())
1307							|| (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().mamSupport())) {
1308						MessageArchiveService.Query query = getMessageArchiveService().query(conversation, 0, timestamp);
1309						if (query != null) {
1310							query.setCallback(callback);
1311						}
1312						callback.informUser(R.string.fetching_history_from_server);
1313					}
1314				}
1315			}
1316		};
1317		mDatabaseExecutor.execute(runnable);
1318	}
1319
1320	public List<Account> getAccounts() {
1321		return this.accounts;
1322	}
1323
1324	public Conversation find(final Iterable<Conversation> haystack, final Contact contact) {
1325		for (final Conversation conversation : haystack) {
1326			if (conversation.getContact() == contact) {
1327				return conversation;
1328			}
1329		}
1330		return null;
1331	}
1332
1333	public Conversation find(final Iterable<Conversation> haystack, final Account account, final Jid jid) {
1334		if (jid == null) {
1335			return null;
1336		}
1337		for (final Conversation conversation : haystack) {
1338			if ((account == null || conversation.getAccount() == account)
1339					&& (conversation.getJid().toBareJid().equals(jid.toBareJid()))) {
1340				return conversation;
1341			}
1342		}
1343		return null;
1344	}
1345
1346	public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc) {
1347		return this.findOrCreateConversation(account, jid, muc, null);
1348	}
1349
1350	public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final MessageArchiveService.Query query) {
1351		synchronized (this.conversations) {
1352			Conversation conversation = find(account, jid);
1353			if (conversation != null) {
1354				return conversation;
1355			}
1356			conversation = databaseBackend.findConversation(account, jid);
1357			if (conversation != null) {
1358				conversation.setStatus(Conversation.STATUS_AVAILABLE);
1359				conversation.setAccount(account);
1360				if (muc) {
1361					conversation.setMode(Conversation.MODE_MULTI);
1362					conversation.setContactJid(jid);
1363				} else {
1364					conversation.setMode(Conversation.MODE_SINGLE);
1365					conversation.setContactJid(jid.toBareJid());
1366				}
1367				conversation.setNextEncryption(-1);
1368				conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
1369				this.databaseBackend.updateConversation(conversation);
1370			} else {
1371				String conversationName;
1372				Contact contact = account.getRoster().getContact(jid);
1373				if (contact != null) {
1374					conversationName = contact.getDisplayName();
1375				} else {
1376					conversationName = jid.getLocalpart();
1377				}
1378				if (muc) {
1379					conversation = new Conversation(conversationName, account, jid,
1380							Conversation.MODE_MULTI);
1381				} else {
1382					conversation = new Conversation(conversationName, account, jid.toBareJid(),
1383							Conversation.MODE_SINGLE);
1384				}
1385				this.databaseBackend.createConversation(conversation);
1386			}
1387			if (account.getXmppConnection() != null
1388					&& account.getXmppConnection().getFeatures().mam()
1389					&& !muc) {
1390				if (query == null) {
1391					this.mMessageArchiveService.query(conversation);
1392				} else {
1393					if (query.getConversation() == null) {
1394						this.mMessageArchiveService.query(conversation, query.getStart());
1395					}
1396				}
1397			}
1398			checkDeletedFiles(conversation);
1399			this.conversations.add(conversation);
1400			updateConversationUi();
1401			return conversation;
1402		}
1403	}
1404
1405	public void archiveConversation(Conversation conversation) {
1406		getNotificationService().clear(conversation);
1407		conversation.setStatus(Conversation.STATUS_ARCHIVED);
1408		conversation.setNextEncryption(-1);
1409		synchronized (this.conversations) {
1410			if (conversation.getMode() == Conversation.MODE_MULTI) {
1411				if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1412					Bookmark bookmark = conversation.getBookmark();
1413					if (bookmark != null && bookmark.autojoin() && respectAutojoin()) {
1414						bookmark.setAutojoin(false);
1415						pushBookmarks(bookmark.getAccount());
1416					}
1417				}
1418				leaveMuc(conversation);
1419			} else {
1420				conversation.endOtrIfNeeded();
1421				if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1422					Log.d(Config.LOGTAG, "Canceling presence request from " + conversation.getJid().toString());
1423					sendPresencePacket(
1424							conversation.getAccount(),
1425							mPresenceGenerator.stopPresenceUpdatesTo(conversation.getContact())
1426					);
1427				}
1428			}
1429			this.databaseBackend.updateConversation(conversation);
1430			this.conversations.remove(conversation);
1431			updateConversationUi();
1432		}
1433	}
1434
1435	public void createAccount(final Account account) {
1436		account.initAccountServices(this);
1437		databaseBackend.createAccount(account);
1438		this.accounts.add(account);
1439		this.reconnectAccountInBackground(account);
1440		updateAccountUi();
1441	}
1442
1443	public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
1444		new Thread(new Runnable() {
1445			@Override
1446			public void run() {
1447				try {
1448					X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
1449					Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
1450					if (findAccountByJid(info.first) == null) {
1451						Account account = new Account(info.first, "");
1452						account.setPrivateKeyAlias(alias);
1453						account.setOption(Account.OPTION_DISABLED, true);
1454						account.setDisplayName(info.second);
1455						createAccount(account);
1456						callback.onAccountCreated(account);
1457						if (Config.X509_VERIFICATION) {
1458							try {
1459								getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
1460							} catch (CertificateException e) {
1461								callback.informUser(R.string.certificate_chain_is_not_trusted);
1462							}
1463						}
1464					} else {
1465						callback.informUser(R.string.account_already_exists);
1466					}
1467				} catch (Exception e) {
1468					e.printStackTrace();
1469					callback.informUser(R.string.unable_to_parse_certificate);
1470				}
1471			}
1472		}).start();
1473
1474	}
1475
1476	public void updateKeyInAccount(final Account account, final String alias) {
1477		Log.d(Config.LOGTAG, "update key in account " + alias);
1478		try {
1479			X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
1480			Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
1481			if (account.getJid().toBareJid().equals(info.first)) {
1482				account.setPrivateKeyAlias(alias);
1483				account.setDisplayName(info.second);
1484				databaseBackend.updateAccount(account);
1485				if (Config.X509_VERIFICATION) {
1486					try {
1487						getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
1488					} catch (CertificateException e) {
1489						showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
1490					}
1491					account.getAxolotlService().regenerateKeys(true);
1492				}
1493			} else {
1494				showErrorToastInUi(R.string.jid_does_not_match_certificate);
1495			}
1496		} catch (Exception e) {
1497			e.printStackTrace();
1498		}
1499	}
1500
1501	public void updateAccount(final Account account) {
1502		this.statusListener.onStatusChanged(account);
1503		databaseBackend.updateAccount(account);
1504		reconnectAccountInBackground(account);
1505		updateAccountUi();
1506		getNotificationService().updateErrorNotification();
1507	}
1508
1509	public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) {
1510		final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword);
1511		sendIqPacket(account, iq, new OnIqPacketReceived() {
1512			@Override
1513			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1514				if (packet.getType() == IqPacket.TYPE.RESULT) {
1515					account.setPassword(newPassword);
1516					databaseBackend.updateAccount(account);
1517					callback.onPasswordChangeSucceeded();
1518				} else {
1519					callback.onPasswordChangeFailed();
1520				}
1521			}
1522		});
1523	}
1524
1525	public void deleteAccount(final Account account) {
1526		synchronized (this.conversations) {
1527			for (final Conversation conversation : conversations) {
1528				if (conversation.getAccount() == account) {
1529					if (conversation.getMode() == Conversation.MODE_MULTI) {
1530						leaveMuc(conversation);
1531					} else if (conversation.getMode() == Conversation.MODE_SINGLE) {
1532						conversation.endOtrIfNeeded();
1533					}
1534					conversations.remove(conversation);
1535				}
1536			}
1537			if (account.getXmppConnection() != null) {
1538				this.disconnect(account, true);
1539			}
1540			Runnable runnable = new Runnable() {
1541				@Override
1542				public void run() {
1543					databaseBackend.deleteAccount(account);
1544				}
1545			};
1546			mDatabaseExecutor.execute(runnable);
1547			this.accounts.remove(account);
1548			updateAccountUi();
1549			getNotificationService().updateErrorNotification();
1550		}
1551	}
1552
1553	public void setOnConversationListChangedListener(OnConversationUpdate listener) {
1554		synchronized (this) {
1555			if (checkListeners()) {
1556				switchToForeground();
1557			}
1558			this.mOnConversationUpdate = listener;
1559			this.mNotificationService.setIsInForeground(true);
1560			if (this.convChangedListenerCount < 2) {
1561				this.convChangedListenerCount++;
1562			}
1563		}
1564	}
1565
1566	public void removeOnConversationListChangedListener() {
1567		synchronized (this) {
1568			this.convChangedListenerCount--;
1569			if (this.convChangedListenerCount <= 0) {
1570				this.convChangedListenerCount = 0;
1571				this.mOnConversationUpdate = null;
1572				this.mNotificationService.setIsInForeground(false);
1573				if (checkListeners()) {
1574					switchToBackground();
1575				}
1576			}
1577		}
1578	}
1579
1580	public void setOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
1581		synchronized (this) {
1582			if (checkListeners()) {
1583				switchToForeground();
1584			}
1585			this.mOnShowErrorToast = onShowErrorToast;
1586			if (this.showErrorToastListenerCount < 2) {
1587				this.showErrorToastListenerCount++;
1588			}
1589		}
1590		this.mOnShowErrorToast = onShowErrorToast;
1591	}
1592
1593	public void removeOnShowErrorToastListener() {
1594		synchronized (this) {
1595			this.showErrorToastListenerCount--;
1596			if (this.showErrorToastListenerCount <= 0) {
1597				this.showErrorToastListenerCount = 0;
1598				this.mOnShowErrorToast = null;
1599				if (checkListeners()) {
1600					switchToBackground();
1601				}
1602			}
1603		}
1604	}
1605
1606	public void setOnAccountListChangedListener(OnAccountUpdate listener) {
1607		synchronized (this) {
1608			if (checkListeners()) {
1609				switchToForeground();
1610			}
1611			this.mOnAccountUpdate = listener;
1612			if (this.accountChangedListenerCount < 2) {
1613				this.accountChangedListenerCount++;
1614			}
1615		}
1616	}
1617
1618	public void removeOnAccountListChangedListener() {
1619		synchronized (this) {
1620			this.accountChangedListenerCount--;
1621			if (this.accountChangedListenerCount <= 0) {
1622				this.mOnAccountUpdate = null;
1623				this.accountChangedListenerCount = 0;
1624				if (checkListeners()) {
1625					switchToBackground();
1626				}
1627			}
1628		}
1629	}
1630
1631	public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
1632		synchronized (this) {
1633			if (checkListeners()) {
1634				switchToForeground();
1635			}
1636			this.mOnCaptchaRequested = listener;
1637			if (this.captchaRequestedListenerCount < 2) {
1638				this.captchaRequestedListenerCount++;
1639			}
1640		}
1641	}
1642
1643	public void removeOnCaptchaRequestedListener() {
1644		synchronized (this) {
1645			this.captchaRequestedListenerCount--;
1646			if (this.captchaRequestedListenerCount <= 0) {
1647				this.mOnCaptchaRequested = null;
1648				this.captchaRequestedListenerCount = 0;
1649				if (checkListeners()) {
1650					switchToBackground();
1651				}
1652			}
1653		}
1654	}
1655
1656	public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
1657		synchronized (this) {
1658			if (checkListeners()) {
1659				switchToForeground();
1660			}
1661			this.mOnRosterUpdate = listener;
1662			if (this.rosterChangedListenerCount < 2) {
1663				this.rosterChangedListenerCount++;
1664			}
1665		}
1666	}
1667
1668	public void removeOnRosterUpdateListener() {
1669		synchronized (this) {
1670			this.rosterChangedListenerCount--;
1671			if (this.rosterChangedListenerCount <= 0) {
1672				this.rosterChangedListenerCount = 0;
1673				this.mOnRosterUpdate = null;
1674				if (checkListeners()) {
1675					switchToBackground();
1676				}
1677			}
1678		}
1679	}
1680
1681	public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
1682		synchronized (this) {
1683			if (checkListeners()) {
1684				switchToForeground();
1685			}
1686			this.mOnUpdateBlocklist = listener;
1687			if (this.updateBlocklistListenerCount < 2) {
1688				this.updateBlocklistListenerCount++;
1689			}
1690		}
1691	}
1692
1693	public void removeOnUpdateBlocklistListener() {
1694		synchronized (this) {
1695			this.updateBlocklistListenerCount--;
1696			if (this.updateBlocklistListenerCount <= 0) {
1697				this.updateBlocklistListenerCount = 0;
1698				this.mOnUpdateBlocklist = null;
1699				if (checkListeners()) {
1700					switchToBackground();
1701				}
1702			}
1703		}
1704	}
1705
1706	public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
1707		synchronized (this) {
1708			if (checkListeners()) {
1709				switchToForeground();
1710			}
1711			this.mOnKeyStatusUpdated = listener;
1712			if (this.keyStatusUpdatedListenerCount < 2) {
1713				this.keyStatusUpdatedListenerCount++;
1714			}
1715		}
1716	}
1717
1718	public void removeOnNewKeysAvailableListener() {
1719		synchronized (this) {
1720			this.keyStatusUpdatedListenerCount--;
1721			if (this.keyStatusUpdatedListenerCount <= 0) {
1722				this.keyStatusUpdatedListenerCount = 0;
1723				this.mOnKeyStatusUpdated = null;
1724				if (checkListeners()) {
1725					switchToBackground();
1726				}
1727			}
1728		}
1729	}
1730
1731	public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
1732		synchronized (this) {
1733			if (checkListeners()) {
1734				switchToForeground();
1735			}
1736			this.mOnMucRosterUpdate = listener;
1737			if (this.mucRosterChangedListenerCount < 2) {
1738				this.mucRosterChangedListenerCount++;
1739			}
1740		}
1741	}
1742
1743	public void removeOnMucRosterUpdateListener() {
1744		synchronized (this) {
1745			this.mucRosterChangedListenerCount--;
1746			if (this.mucRosterChangedListenerCount <= 0) {
1747				this.mucRosterChangedListenerCount = 0;
1748				this.mOnMucRosterUpdate = null;
1749				if (checkListeners()) {
1750					switchToBackground();
1751				}
1752			}
1753		}
1754	}
1755
1756	private boolean checkListeners() {
1757		return (this.mOnAccountUpdate == null
1758				&& this.mOnConversationUpdate == null
1759				&& this.mOnRosterUpdate == null
1760				&& this.mOnCaptchaRequested == null
1761				&& this.mOnUpdateBlocklist == null
1762				&& this.mOnShowErrorToast == null
1763				&& this.mOnKeyStatusUpdated == null);
1764	}
1765
1766	private void switchToForeground() {
1767		for (Conversation conversation : getConversations()) {
1768			conversation.setIncomingChatState(ChatState.ACTIVE);
1769		}
1770		for (Account account : getAccounts()) {
1771			if (account.getStatus() == Account.State.ONLINE) {
1772				XmppConnection connection = account.getXmppConnection();
1773				if (connection != null && connection.getFeatures().csi()) {
1774					connection.sendActive();
1775				}
1776			}
1777		}
1778		Log.d(Config.LOGTAG, "app switched into foreground");
1779	}
1780
1781	private void switchToBackground() {
1782		for (Account account : getAccounts()) {
1783			if (account.getStatus() == Account.State.ONLINE) {
1784				XmppConnection connection = account.getXmppConnection();
1785				if (connection != null) {
1786					if (connection.getFeatures().csi()) {
1787						connection.sendInactive();
1788					}
1789					if (Config.CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND && mPushManagementService.available(account)) {
1790						connection.waitForPush();
1791						cancelWakeUpCall(account.getUuid().hashCode());
1792					}
1793				}
1794			}
1795		}
1796		this.mNotificationService.setIsInForeground(false);
1797		Log.d(Config.LOGTAG, "app switched into background");
1798	}
1799
1800	private void connectMultiModeConversations(Account account) {
1801		List<Conversation> conversations = getConversations();
1802		for (Conversation conversation : conversations) {
1803			if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) {
1804				joinMuc(conversation);
1805			}
1806		}
1807	}
1808
1809	public void joinMuc(Conversation conversation) {
1810		joinMuc(conversation, null);
1811	}
1812
1813	private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
1814		Account account = conversation.getAccount();
1815		account.pendingConferenceJoins.remove(conversation);
1816		account.pendingConferenceLeaves.remove(conversation);
1817		if (account.getStatus() == Account.State.ONLINE) {
1818			conversation.resetMucOptions();
1819			conversation.setHasMessagesLeftOnServer(false);
1820			fetchConferenceConfiguration(conversation, new OnConferenceConfigurationFetched() {
1821
1822				private void join(Conversation conversation) {
1823					Account account = conversation.getAccount();
1824					final MucOptions mucOptions = conversation.getMucOptions();
1825					final Jid joinJid = mucOptions.getSelf().getFullJid();
1826					Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString());
1827					PresencePacket packet = new PresencePacket();
1828					packet.setFrom(conversation.getAccount().getJid());
1829					packet.setTo(joinJid);
1830					Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
1831					if (conversation.getMucOptions().getPassword() != null) {
1832						x.addChild("password").setContent(conversation.getMucOptions().getPassword());
1833					}
1834
1835					if (mucOptions.mamSupport()) {
1836						// Use MAM instead of the limited muc history to get history
1837						x.addChild("history").setAttribute("maxchars", "0");
1838					} else {
1839						// Fallback to muc history
1840						x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted()));
1841					}
1842					String sig = account.getPgpSignature();
1843					if (sig != null) {
1844						packet.addChild("x", "jabber:x:signed").setContent(sig);
1845					}
1846					sendPresencePacket(account, packet);
1847					if (onConferenceJoined != null) {
1848						onConferenceJoined.onConferenceJoined(conversation);
1849					}
1850					if (!joinJid.equals(conversation.getJid())) {
1851						conversation.setContactJid(joinJid);
1852						databaseBackend.updateConversation(conversation);
1853					}
1854
1855					if (mucOptions.mamSupport()) {
1856						getMessageArchiveService().catchupMUC(conversation);
1857					}
1858					if (mucOptions.membersOnly() && mucOptions.nonanonymous()) {
1859						fetchConferenceMembers(conversation);
1860					}
1861					sendUnsentMessages(conversation);
1862				}
1863
1864				@Override
1865				public void onConferenceConfigurationFetched(Conversation conversation) {
1866					join(conversation);
1867				}
1868
1869				@Override
1870				public void onFetchFailed(final Conversation conversation, Element error) {
1871					join(conversation);
1872					fetchConferenceConfiguration(conversation);
1873				}
1874			});
1875			updateConversationUi();
1876		} else {
1877			account.pendingConferenceJoins.add(conversation);
1878			conversation.resetMucOptions();
1879			conversation.setHasMessagesLeftOnServer(false);
1880			updateConversationUi();
1881		}
1882	}
1883
1884	private void fetchConferenceMembers(final Conversation conversation) {
1885		final Account account = conversation.getAccount();
1886		final String[] affiliations = {"member","admin","owner"};
1887		OnIqPacketReceived callback = new OnIqPacketReceived() {
1888
1889			private int i = 0;
1890
1891			@Override
1892			public void onIqPacketReceived(Account account, IqPacket packet) {
1893				Element query = packet.query("http://jabber.org/protocol/muc#admin");
1894				if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
1895					for(Element child : query.getChildren()) {
1896						if ("item".equals(child.getName())) {
1897							conversation.getMucOptions().putMember(child.getAttributeAsJid("jid"));
1898						}
1899					}
1900				} else {
1901					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not request affiliation "+affiliations[i]+" in "+conversation.getJid().toBareJid());
1902				}
1903				++i;
1904				if (i >= affiliations.length) {
1905					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved members for "+conversation.getJid().toBareJid()+": "+conversation.getMucOptions().getMembers());
1906				}
1907			}
1908		};
1909		for(String affiliation : affiliations) {
1910			sendIqPacket(account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
1911		}
1912		Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetching members for "+conversation.getName());
1913	}
1914
1915	public void providePasswordForMuc(Conversation conversation, String password) {
1916		if (conversation.getMode() == Conversation.MODE_MULTI) {
1917			conversation.getMucOptions().setPassword(password);
1918			if (conversation.getBookmark() != null) {
1919				if (respectAutojoin()) {
1920					conversation.getBookmark().setAutojoin(true);
1921				}
1922				pushBookmarks(conversation.getAccount());
1923			}
1924			databaseBackend.updateConversation(conversation);
1925			joinMuc(conversation);
1926		}
1927	}
1928
1929	public void renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
1930		final MucOptions options = conversation.getMucOptions();
1931		final Jid joinJid = options.createJoinJid(nick);
1932		if (options.online()) {
1933			Account account = conversation.getAccount();
1934			options.setOnRenameListener(new OnRenameListener() {
1935
1936				@Override
1937				public void onSuccess() {
1938					conversation.setContactJid(joinJid);
1939					databaseBackend.updateConversation(conversation);
1940					Bookmark bookmark = conversation.getBookmark();
1941					if (bookmark != null) {
1942						bookmark.setNick(nick);
1943						pushBookmarks(bookmark.getAccount());
1944					}
1945					callback.success(conversation);
1946				}
1947
1948				@Override
1949				public void onFailure() {
1950					callback.error(R.string.nick_in_use, conversation);
1951				}
1952			});
1953
1954			PresencePacket packet = new PresencePacket();
1955			packet.setTo(joinJid);
1956			packet.setFrom(conversation.getAccount().getJid());
1957
1958			String sig = account.getPgpSignature();
1959			if (sig != null) {
1960				packet.addChild("status").setContent("online");
1961				packet.addChild("x", "jabber:x:signed").setContent(sig);
1962			}
1963			sendPresencePacket(account, packet);
1964		} else {
1965			conversation.setContactJid(joinJid);
1966			databaseBackend.updateConversation(conversation);
1967			if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1968				Bookmark bookmark = conversation.getBookmark();
1969				if (bookmark != null) {
1970					bookmark.setNick(nick);
1971					pushBookmarks(bookmark.getAccount());
1972				}
1973				joinMuc(conversation);
1974			}
1975		}
1976	}
1977
1978	public void leaveMuc(Conversation conversation) {
1979		leaveMuc(conversation, false);
1980	}
1981
1982	private void leaveMuc(Conversation conversation, boolean now) {
1983		Account account = conversation.getAccount();
1984		account.pendingConferenceJoins.remove(conversation);
1985		account.pendingConferenceLeaves.remove(conversation);
1986		if (account.getStatus() == Account.State.ONLINE || now) {
1987			PresencePacket packet = new PresencePacket();
1988			packet.setTo(conversation.getMucOptions().getSelf().getFullJid());
1989			packet.setFrom(conversation.getAccount().getJid());
1990			packet.setAttribute("type", "unavailable");
1991			sendPresencePacket(conversation.getAccount(), packet);
1992			conversation.getMucOptions().setOffline();
1993			conversation.deregisterWithBookmark();
1994			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
1995					+ ": leaving muc " + conversation.getJid());
1996		} else {
1997			account.pendingConferenceLeaves.add(conversation);
1998		}
1999	}
2000
2001	private String findConferenceServer(final Account account) {
2002		String server;
2003		if (account.getXmppConnection() != null) {
2004			server = account.getXmppConnection().getMucServer();
2005			if (server != null) {
2006				return server;
2007			}
2008		}
2009		for (Account other : getAccounts()) {
2010			if (other != account && other.getXmppConnection() != null) {
2011				server = other.getXmppConnection().getMucServer();
2012				if (server != null) {
2013					return server;
2014				}
2015			}
2016		}
2017		return null;
2018	}
2019
2020	public void createAdhocConference(final Account account, final Iterable<Jid> jids, final UiCallback<Conversation> callback) {
2021		Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString());
2022		if (account.getStatus() == Account.State.ONLINE) {
2023			try {
2024				String server = findConferenceServer(account);
2025				if (server == null) {
2026					if (callback != null) {
2027						callback.error(R.string.no_conference_server_found, null);
2028					}
2029					return;
2030				}
2031				String name = new BigInteger(75, getRNG()).toString(32);
2032				Jid jid = Jid.fromParts(name, server, null);
2033				final Conversation conversation = findOrCreateConversation(account, jid, true);
2034				joinMuc(conversation, new OnConferenceJoined() {
2035					@Override
2036					public void onConferenceJoined(final Conversation conversation) {
2037						Bundle options = new Bundle();
2038						options.putString("muc#roomconfig_persistentroom", "1");
2039						options.putString("muc#roomconfig_membersonly", "1");
2040						options.putString("muc#roomconfig_publicroom", "0");
2041						options.putString("muc#roomconfig_whois", "anyone");
2042						pushConferenceConfiguration(conversation, options, new OnConferenceOptionsPushed() {
2043							@Override
2044							public void onPushSucceeded() {
2045								for (Jid invite : jids) {
2046									invite(conversation, invite);
2047								}
2048								if (account.countPresences() > 1) {
2049									directInvite(conversation, account.getJid().toBareJid());
2050								}
2051								if (callback != null) {
2052									callback.success(conversation);
2053								}
2054							}
2055
2056							@Override
2057							public void onPushFailed() {
2058								if (callback != null) {
2059									callback.error(R.string.conference_creation_failed, conversation);
2060								}
2061							}
2062						});
2063					}
2064				});
2065			} catch (InvalidJidException e) {
2066				if (callback != null) {
2067					callback.error(R.string.conference_creation_failed, null);
2068				}
2069			}
2070		} else {
2071			if (callback != null) {
2072				callback.error(R.string.not_connected_try_again, null);
2073			}
2074		}
2075	}
2076
2077	public void fetchConferenceConfiguration(final Conversation conversation) {
2078		fetchConferenceConfiguration(conversation, null);
2079	}
2080
2081	public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) {
2082		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2083		request.setTo(conversation.getJid().toBareJid());
2084		request.query("http://jabber.org/protocol/disco#info");
2085		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
2086			@Override
2087			public void onIqPacketReceived(Account account, IqPacket packet) {
2088				Element query = packet.findChild("query","http://jabber.org/protocol/disco#info");
2089				if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
2090					ArrayList<String> features = new ArrayList<>();
2091					for (Element child : query.getChildren()) {
2092						if (child != null && child.getName().equals("feature")) {
2093							String var = child.getAttribute("var");
2094							if (var != null) {
2095								features.add(var);
2096							}
2097						}
2098					}
2099					Element form = query.findChild("x", "jabber:x:data");
2100					if (form != null) {
2101						conversation.getMucOptions().updateFormData(Data.parse(form));
2102					}
2103					conversation.getMucOptions().updateFeatures(features);
2104					if (callback != null) {
2105						callback.onConferenceConfigurationFetched(conversation);
2106					}
2107					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetched muc configuration for "+conversation.getJid().toBareJid()+" - "+features.toString());
2108					updateConversationUi();
2109				} else if (packet.getType() == IqPacket.TYPE.ERROR) {
2110					if (callback != null) {
2111						callback.onFetchFailed(conversation, packet.getError());
2112					}
2113				}
2114			}
2115		});
2116	}
2117
2118	public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConferenceOptionsPushed callback) {
2119		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2120		request.setTo(conversation.getJid().toBareJid());
2121		request.query("http://jabber.org/protocol/muc#owner");
2122		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
2123			@Override
2124			public void onIqPacketReceived(Account account, IqPacket packet) {
2125				if (packet.getType() == IqPacket.TYPE.RESULT) {
2126					Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
2127					for (Field field : data.getFields()) {
2128						if (options.containsKey(field.getFieldName())) {
2129							field.setValue(options.getString(field.getFieldName()));
2130						}
2131					}
2132					data.submit();
2133					IqPacket set = new IqPacket(IqPacket.TYPE.SET);
2134					set.setTo(conversation.getJid().toBareJid());
2135					set.query("http://jabber.org/protocol/muc#owner").addChild(data);
2136					sendIqPacket(account, set, new OnIqPacketReceived() {
2137						@Override
2138						public void onIqPacketReceived(Account account, IqPacket packet) {
2139							if (callback != null) {
2140								if (packet.getType() == IqPacket.TYPE.RESULT) {
2141									callback.onPushSucceeded();
2142								} else {
2143									callback.onPushFailed();
2144								}
2145							}
2146						}
2147					});
2148				} else {
2149					if (callback != null) {
2150						callback.onPushFailed();
2151					}
2152				}
2153			}
2154		});
2155	}
2156
2157	public void pushSubjectToConference(final Conversation conference, final String subject) {
2158		MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, subject);
2159		this.sendMessagePacket(conference.getAccount(), packet);
2160		final MucOptions mucOptions = conference.getMucOptions();
2161		final MucOptions.User self = mucOptions.getSelf();
2162		if (!mucOptions.persistent() && self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
2163			Bundle options = new Bundle();
2164			options.putString("muc#roomconfig_persistentroom", "1");
2165			this.pushConferenceConfiguration(conference, options, null);
2166		}
2167	}
2168
2169	public void changeAffiliationInConference(final Conversation conference, Jid user, MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
2170		final Jid jid = user.toBareJid();
2171		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
2172		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
2173			@Override
2174			public void onIqPacketReceived(Account account, IqPacket packet) {
2175				if (packet.getType() == IqPacket.TYPE.RESULT) {
2176					callback.onAffiliationChangedSuccessful(jid);
2177				} else {
2178					callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
2179				}
2180			}
2181		});
2182	}
2183
2184	public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
2185		List<Jid> jids = new ArrayList<>();
2186		for (MucOptions.User user : conference.getMucOptions().getUsers()) {
2187			if (user.getAffiliation() == before && user.getJid() != null) {
2188				jids.add(user.getJid());
2189			}
2190		}
2191		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
2192		sendIqPacket(conference.getAccount(), request, mDefaultIqHandler);
2193	}
2194
2195	public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) {
2196		IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
2197		Log.d(Config.LOGTAG, request.toString());
2198		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
2199			@Override
2200			public void onIqPacketReceived(Account account, IqPacket packet) {
2201				Log.d(Config.LOGTAG, packet.toString());
2202				if (packet.getType() == IqPacket.TYPE.RESULT) {
2203					callback.onRoleChangedSuccessful(nick);
2204				} else {
2205					callback.onRoleChangeFailed(nick, R.string.could_not_change_role);
2206				}
2207			}
2208		});
2209	}
2210
2211	private void disconnect(Account account, boolean force) {
2212		if ((account.getStatus() == Account.State.ONLINE)
2213				|| (account.getStatus() == Account.State.DISABLED)) {
2214			if (!force) {
2215				List<Conversation> conversations = getConversations();
2216				for (Conversation conversation : conversations) {
2217					if (conversation.getAccount() == account) {
2218						if (conversation.getMode() == Conversation.MODE_MULTI) {
2219							leaveMuc(conversation, true);
2220						} else {
2221							if (conversation.endOtrIfNeeded()) {
2222								Log.d(Config.LOGTAG, account.getJid().toBareJid()
2223										+ ": ended otr session with "
2224										+ conversation.getJid());
2225							}
2226						}
2227					}
2228				}
2229				sendOfflinePresence(account);
2230			}
2231			account.getXmppConnection().disconnect(force);
2232		}
2233	}
2234
2235	@Override
2236	public IBinder onBind(Intent intent) {
2237		return mBinder;
2238	}
2239
2240	public void updateMessage(Message message) {
2241		databaseBackend.updateMessage(message);
2242		updateConversationUi();
2243	}
2244
2245	public void updateMessage(Message message, String uuid) {
2246		databaseBackend.updateMessage(message, uuid);
2247		updateConversationUi();
2248	}
2249
2250	protected void syncDirtyContacts(Account account) {
2251		for (Contact contact : account.getRoster().getContacts()) {
2252			if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
2253				pushContactToServer(contact);
2254			}
2255			if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
2256				deleteContactOnServer(contact);
2257			}
2258		}
2259	}
2260
2261	public void createContact(Contact contact) {
2262		boolean autoGrant = getPreferences().getBoolean("grant_new_contacts", true);
2263		if (autoGrant) {
2264			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
2265			contact.setOption(Contact.Options.ASKING);
2266		}
2267		pushContactToServer(contact);
2268	}
2269
2270	public void onOtrSessionEstablished(Conversation conversation) {
2271		final Account account = conversation.getAccount();
2272		final Session otrSession = conversation.getOtrSession();
2273		Log.d(Config.LOGTAG,
2274				account.getJid().toBareJid() + " otr session established with "
2275						+ conversation.getJid() + "/"
2276						+ otrSession.getSessionID().getUserID());
2277		conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
2278
2279			@Override
2280			public void onMessageFound(Message message) {
2281				SessionID id = otrSession.getSessionID();
2282				try {
2283					message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
2284				} catch (InvalidJidException e) {
2285					return;
2286				}
2287				if (message.needsUploading()) {
2288					mJingleConnectionManager.createNewConnection(message);
2289				} else {
2290					MessagePacket outPacket = mMessageGenerator.generateOtrChat(message);
2291					if (outPacket != null) {
2292						mMessageGenerator.addDelay(outPacket, message.getTimeSent());
2293						message.setStatus(Message.STATUS_SEND);
2294						databaseBackend.updateMessage(message);
2295						sendMessagePacket(account, outPacket);
2296					}
2297				}
2298				updateConversationUi();
2299			}
2300		});
2301	}
2302
2303	public boolean renewSymmetricKey(Conversation conversation) {
2304		Account account = conversation.getAccount();
2305		byte[] symmetricKey = new byte[32];
2306		this.mRandom.nextBytes(symmetricKey);
2307		Session otrSession = conversation.getOtrSession();
2308		if (otrSession != null) {
2309			MessagePacket packet = new MessagePacket();
2310			packet.setType(MessagePacket.TYPE_CHAT);
2311			packet.setFrom(account.getJid());
2312			MessageGenerator.addMessageHints(packet);
2313			packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
2314					+ otrSession.getSessionID().getUserID());
2315			try {
2316				packet.setBody(otrSession
2317						.transformSending(CryptoHelper.FILETRANSFER
2318								+ CryptoHelper.bytesToHex(symmetricKey))[0]);
2319				sendMessagePacket(account, packet);
2320				conversation.setSymmetricKey(symmetricKey);
2321				return true;
2322			} catch (OtrException e) {
2323				return false;
2324			}
2325		}
2326		return false;
2327	}
2328
2329	public void pushContactToServer(final Contact contact) {
2330		contact.resetOption(Contact.Options.DIRTY_DELETE);
2331		contact.setOption(Contact.Options.DIRTY_PUSH);
2332		final Account account = contact.getAccount();
2333		if (account.getStatus() == Account.State.ONLINE) {
2334			final boolean ask = contact.getOption(Contact.Options.ASKING);
2335			final boolean sendUpdates = contact
2336					.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
2337					&& contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
2338			final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2339			iq.query(Xmlns.ROSTER).addChild(contact.asElement());
2340			account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
2341			if (sendUpdates) {
2342				sendPresencePacket(account,
2343						mPresenceGenerator.sendPresenceUpdatesTo(contact));
2344			}
2345			if (ask) {
2346				sendPresencePacket(account,
2347						mPresenceGenerator.requestPresenceUpdatesFrom(contact));
2348			}
2349		}
2350	}
2351
2352	public void publishAvatar(Account account, Uri image, UiCallback<Avatar> callback) {
2353		final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
2354		final int size = Config.AVATAR_SIZE;
2355		final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
2356		if (avatar != null) {
2357			avatar.height = size;
2358			avatar.width = size;
2359			if (format.equals(Bitmap.CompressFormat.WEBP)) {
2360				avatar.type = "image/webp";
2361			} else if (format.equals(Bitmap.CompressFormat.JPEG)) {
2362				avatar.type = "image/jpeg";
2363			} else if (format.equals(Bitmap.CompressFormat.PNG)) {
2364				avatar.type = "image/png";
2365			}
2366			if (!getFileBackend().save(avatar)) {
2367				callback.error(R.string.error_saving_avatar, avatar);
2368				return;
2369			}
2370			publishAvatar(account, avatar, callback);
2371		} else {
2372			callback.error(R.string.error_publish_avatar_converting, null);
2373		}
2374	}
2375
2376	public void publishAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2377		final IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
2378		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2379
2380			@Override
2381			public void onIqPacketReceived(Account account, IqPacket result) {
2382				if (result.getType() == IqPacket.TYPE.RESULT) {
2383					final IqPacket packet = XmppConnectionService.this.mIqGenerator
2384							.publishAvatarMetadata(avatar);
2385					sendIqPacket(account, packet, new OnIqPacketReceived() {
2386						@Override
2387						public void onIqPacketReceived(Account account, IqPacket result) {
2388							if (result.getType() == IqPacket.TYPE.RESULT) {
2389								if (account.setAvatar(avatar.getFilename())) {
2390									getAvatarService().clear(account);
2391									databaseBackend.updateAccount(account);
2392								}
2393								if (callback != null) {
2394									callback.success(avatar);
2395								} else {
2396									Log.d(Config.LOGTAG,account.getJid().toBareJid()+": published avatar");
2397								}
2398							} else {
2399								if (callback != null) {
2400									callback.error(
2401											R.string.error_publish_avatar_server_reject,
2402											avatar);
2403								}
2404							}
2405						}
2406					});
2407				} else {
2408					if (callback != null) {
2409						callback.error(
2410								R.string.error_publish_avatar_server_reject,
2411								avatar);
2412					}
2413				}
2414			}
2415		});
2416	}
2417
2418	public void republishAvatarIfNeeded(Account account) {
2419		if (account.getAxolotlService().isPepBroken()) {
2420			Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping republication of avatar because pep is broken");
2421			return;
2422		}
2423		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
2424		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2425
2426			private Avatar parseAvatar(IqPacket packet) {
2427				Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
2428				if (pubsub != null) {
2429					Element items = pubsub.findChild("items");
2430					if (items != null) {
2431						return Avatar.parseMetadata(items);
2432					}
2433				}
2434				return null;
2435			}
2436
2437			private boolean errorIsItemNotFound(IqPacket packet) {
2438				Element error = packet.findChild("error");
2439				return packet.getType() == IqPacket.TYPE.ERROR
2440						&& error != null
2441						&& error.hasChild("item-not-found");
2442			}
2443
2444			@Override
2445			public void onIqPacketReceived(Account account, IqPacket packet) {
2446				if (packet.getType() == IqPacket.TYPE.RESULT || errorIsItemNotFound(packet)) {
2447					Avatar serverAvatar = parseAvatar(packet);
2448					if (serverAvatar == null && account.getAvatar() != null) {
2449						Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar());
2450						if (avatar != null) {
2451							Log.d(Config.LOGTAG,account.getJid().toBareJid()+": avatar on server was null. republishing");
2452							publishAvatar(account, fileBackend.getStoredPepAvatar(account.getAvatar()), null);
2453						} else {
2454							Log.e(Config.LOGTAG, account.getJid().toBareJid()+": error rereading avatar");
2455						}
2456					}
2457				}
2458			}
2459		});
2460	}
2461
2462	public void fetchAvatar(Account account, Avatar avatar) {
2463		fetchAvatar(account, avatar, null);
2464	}
2465
2466	public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2467		final String KEY = generateFetchKey(account, avatar);
2468		synchronized (this.mInProgressAvatarFetches) {
2469			if (!this.mInProgressAvatarFetches.contains(KEY)) {
2470				switch (avatar.origin) {
2471					case PEP:
2472						this.mInProgressAvatarFetches.add(KEY);
2473						fetchAvatarPep(account, avatar, callback);
2474						break;
2475					case VCARD:
2476						this.mInProgressAvatarFetches.add(KEY);
2477						fetchAvatarVcard(account, avatar, callback);
2478						break;
2479				}
2480			}
2481		}
2482	}
2483
2484	private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2485		IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar);
2486		sendIqPacket(account, packet, new OnIqPacketReceived() {
2487
2488			@Override
2489			public void onIqPacketReceived(Account account, IqPacket result) {
2490				synchronized (mInProgressAvatarFetches) {
2491					mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
2492				}
2493				final String ERROR = account.getJid().toBareJid()
2494						+ ": fetching avatar for " + avatar.owner + " failed ";
2495				if (result.getType() == IqPacket.TYPE.RESULT) {
2496					avatar.image = mIqParser.avatarData(result);
2497					if (avatar.image != null) {
2498						if (getFileBackend().save(avatar)) {
2499							if (account.getJid().toBareJid().equals(avatar.owner)) {
2500								if (account.setAvatar(avatar.getFilename())) {
2501									databaseBackend.updateAccount(account);
2502								}
2503								getAvatarService().clear(account);
2504								updateConversationUi();
2505								updateAccountUi();
2506							} else {
2507								Contact contact = account.getRoster()
2508										.getContact(avatar.owner);
2509								contact.setAvatar(avatar);
2510								getAvatarService().clear(contact);
2511								updateConversationUi();
2512								updateRosterUi();
2513							}
2514							if (callback != null) {
2515								callback.success(avatar);
2516							}
2517							Log.d(Config.LOGTAG, account.getJid().toBareJid()
2518									+ ": succesfuly fetched pep avatar for " + avatar.owner);
2519							return;
2520						}
2521					} else {
2522
2523						Log.d(Config.LOGTAG, ERROR + "(parsing error)");
2524					}
2525				} else {
2526					Element error = result.findChild("error");
2527					if (error == null) {
2528						Log.d(Config.LOGTAG, ERROR + "(server error)");
2529					} else {
2530						Log.d(Config.LOGTAG, ERROR + error.toString());
2531					}
2532				}
2533				if (callback != null) {
2534					callback.error(0, null);
2535				}
2536
2537			}
2538		});
2539	}
2540
2541	private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2542		IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
2543		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2544			@Override
2545			public void onIqPacketReceived(Account account, IqPacket packet) {
2546				synchronized (mInProgressAvatarFetches) {
2547					mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
2548				}
2549				if (packet.getType() == IqPacket.TYPE.RESULT) {
2550					Element vCard = packet.findChild("vCard", "vcard-temp");
2551					Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
2552					String image = photo != null ? photo.findChildContent("BINVAL") : null;
2553					if (image != null) {
2554						avatar.image = image;
2555						if (getFileBackend().save(avatar)) {
2556							Log.d(Config.LOGTAG, account.getJid().toBareJid()
2557									+ ": successfully fetched vCard avatar for " + avatar.owner);
2558							if (avatar.owner.isBareJid()) {
2559								Contact contact = account.getRoster()
2560										.getContact(avatar.owner);
2561								contact.setAvatar(avatar);
2562								getAvatarService().clear(contact);
2563								updateConversationUi();
2564								updateRosterUi();
2565							} else {
2566								Conversation conversation = find(account, avatar.owner.toBareJid());
2567								if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
2568									MucOptions.User user = conversation.getMucOptions().findUser(avatar.owner.getResourcepart());
2569									if (user != null) {
2570										if (user.setAvatar(avatar)) {
2571											getAvatarService().clear(user);
2572											updateConversationUi();
2573											updateMucRosterUi();
2574										}
2575									}
2576								}
2577							}
2578						}
2579					}
2580				}
2581			}
2582		});
2583	}
2584
2585	public void checkForAvatar(Account account, final UiCallback<Avatar> callback) {
2586		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
2587		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2588
2589			@Override
2590			public void onIqPacketReceived(Account account, IqPacket packet) {
2591				if (packet.getType() == IqPacket.TYPE.RESULT) {
2592					Element pubsub = packet.findChild("pubsub","http://jabber.org/protocol/pubsub");
2593					if (pubsub != null) {
2594						Element items = pubsub.findChild("items");
2595						if (items != null) {
2596							Avatar avatar = Avatar.parseMetadata(items);
2597							if (avatar != null) {
2598								avatar.owner = account.getJid().toBareJid();
2599								if (fileBackend.isAvatarCached(avatar)) {
2600									if (account.setAvatar(avatar.getFilename())) {
2601										databaseBackend.updateAccount(account);
2602									}
2603									getAvatarService().clear(account);
2604									callback.success(avatar);
2605								} else {
2606									fetchAvatarPep(account, avatar, callback);
2607								}
2608								return;
2609							}
2610						}
2611					}
2612				}
2613				callback.error(0, null);
2614			}
2615		});
2616	}
2617
2618	public void deleteContactOnServer(Contact contact) {
2619		contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
2620		contact.resetOption(Contact.Options.DIRTY_PUSH);
2621		contact.setOption(Contact.Options.DIRTY_DELETE);
2622		Account account = contact.getAccount();
2623		if (account.getStatus() == Account.State.ONLINE) {
2624			IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2625			Element item = iq.query(Xmlns.ROSTER).addChild("item");
2626			item.setAttribute("jid", contact.getJid().toString());
2627			item.setAttribute("subscription", "remove");
2628			account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
2629		}
2630	}
2631
2632	public void updateConversation(Conversation conversation) {
2633		this.databaseBackend.updateConversation(conversation);
2634	}
2635
2636	private void reconnectAccount(final Account account, final boolean force, final boolean interactive) {
2637		synchronized (account) {
2638			XmppConnection connection = account.getXmppConnection();
2639			if (connection == null) {
2640				connection = createConnection(account);
2641				account.setXmppConnection(connection);
2642			}
2643			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
2644				if (!force) {
2645					disconnect(account, false);
2646				}
2647				Thread thread = new Thread(connection);
2648				connection.setInteractive(interactive);
2649				connection.prepareNewConnection();
2650				thread.start();
2651				scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
2652			} else {
2653				disconnect(account, force);
2654				account.getRoster().clearPresences();
2655				connection.resetEverything();
2656				account.getAxolotlService().resetBrokenness();
2657			}
2658		}
2659	}
2660
2661	public void reconnectAccountInBackground(final Account account) {
2662		new Thread(new Runnable() {
2663			@Override
2664			public void run() {
2665				reconnectAccount(account, false, true);
2666			}
2667		}).start();
2668	}
2669
2670	public void invite(Conversation conversation, Jid contact) {
2671		Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": inviting " + contact + " to " + conversation.getJid().toBareJid());
2672		MessagePacket packet = mMessageGenerator.invite(conversation, contact);
2673		sendMessagePacket(conversation.getAccount(), packet);
2674	}
2675
2676	public void directInvite(Conversation conversation, Jid jid) {
2677		MessagePacket packet = mMessageGenerator.directInvite(conversation, jid);
2678		sendMessagePacket(conversation.getAccount(), packet);
2679	}
2680
2681	public void resetSendingToWaiting(Account account) {
2682		for (Conversation conversation : getConversations()) {
2683			if (conversation.getAccount() == account) {
2684				conversation.findUnsentTextMessages(new Conversation.OnMessageFound() {
2685
2686					@Override
2687					public void onMessageFound(Message message) {
2688						markMessage(message, Message.STATUS_WAITING);
2689					}
2690				});
2691			}
2692		}
2693	}
2694
2695	public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) {
2696		if (uuid == null) {
2697			return null;
2698		}
2699		for (Conversation conversation : getConversations()) {
2700			if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) {
2701				final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
2702				if (message != null) {
2703					markMessage(message, status);
2704				}
2705				return message;
2706			}
2707		}
2708		return null;
2709	}
2710
2711	public boolean markMessage(Conversation conversation, String uuid, int status) {
2712		if (uuid == null) {
2713			return false;
2714		} else {
2715			Message message = conversation.findSentMessageWithUuid(uuid);
2716			if (message != null) {
2717				markMessage(message, status);
2718				return true;
2719			} else {
2720				return false;
2721			}
2722		}
2723	}
2724
2725	public void markMessage(Message message, int status) {
2726		if (status == Message.STATUS_SEND_FAILED
2727				&& (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
2728				.getStatus() == Message.STATUS_SEND_DISPLAYED)) {
2729			return;
2730		}
2731		message.setStatus(status);
2732		databaseBackend.updateMessage(message);
2733		updateConversationUi();
2734	}
2735
2736	public SharedPreferences getPreferences() {
2737		return PreferenceManager
2738				.getDefaultSharedPreferences(getApplicationContext());
2739	}
2740
2741	public boolean confirmMessages() {
2742		return getPreferences().getBoolean("confirm_messages", true);
2743	}
2744
2745	public boolean allowMessageCorrection() {
2746		return getPreferences().getBoolean("allow_message_correction", false);
2747	}
2748
2749	public boolean sendChatStates() {
2750		return getPreferences().getBoolean("chat_states", false);
2751	}
2752
2753	public boolean saveEncryptedMessages() {
2754		return !getPreferences().getBoolean("dont_save_encrypted", false);
2755	}
2756
2757	private boolean respectAutojoin() {
2758		return getPreferences().getBoolean("autojoin", true);
2759	}
2760
2761	public boolean indicateReceived() {
2762		return getPreferences().getBoolean("indicate_received", false);
2763	}
2764
2765	public boolean useTorToConnect() {
2766		return Config.FORCE_ORBOT || getPreferences().getBoolean("use_tor", false);
2767	}
2768
2769	public boolean showExtendedConnectionOptions() {
2770		return getPreferences().getBoolean("show_connection_options", false);
2771	}
2772
2773	public int unreadCount() {
2774		int count = 0;
2775		for (Conversation conversation : getConversations()) {
2776			count += conversation.unreadCount();
2777		}
2778		return count;
2779	}
2780
2781
2782	public void showErrorToastInUi(int resId) {
2783		if (mOnShowErrorToast != null) {
2784			mOnShowErrorToast.onShowErrorToast(resId);
2785		}
2786	}
2787
2788	public void updateConversationUi() {
2789		if (mOnConversationUpdate != null) {
2790			mOnConversationUpdate.onConversationUpdate();
2791		}
2792	}
2793
2794	public void updateAccountUi() {
2795		if (mOnAccountUpdate != null) {
2796			mOnAccountUpdate.onAccountUpdate();
2797		}
2798	}
2799
2800	public void updateRosterUi() {
2801		if (mOnRosterUpdate != null) {
2802			mOnRosterUpdate.onRosterUpdate();
2803		}
2804	}
2805
2806	public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
2807		boolean rc = false;
2808		if (mOnCaptchaRequested != null) {
2809			DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
2810			Bitmap scaled = Bitmap.createScaledBitmap(captcha, (int) (captcha.getWidth() * metrics.scaledDensity),
2811					(int) (captcha.getHeight() * metrics.scaledDensity), false);
2812
2813			mOnCaptchaRequested.onCaptchaRequested(account, id, data, scaled);
2814			rc = true;
2815		}
2816
2817		return rc;
2818	}
2819
2820	public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
2821		if (mOnUpdateBlocklist != null) {
2822			mOnUpdateBlocklist.OnUpdateBlocklist(status);
2823		}
2824	}
2825
2826	public void updateMucRosterUi() {
2827		if (mOnMucRosterUpdate != null) {
2828			mOnMucRosterUpdate.onMucRosterUpdate();
2829		}
2830	}
2831
2832	public void keyStatusUpdated(AxolotlService.FetchStatus report) {
2833		if (mOnKeyStatusUpdated != null) {
2834			mOnKeyStatusUpdated.onKeyStatusUpdated(report);
2835		}
2836	}
2837
2838	public Account findAccountByJid(final Jid accountJid) {
2839		for (Account account : this.accounts) {
2840			if (account.getJid().toBareJid().equals(accountJid.toBareJid())) {
2841				return account;
2842			}
2843		}
2844		return null;
2845	}
2846
2847	public Conversation findConversationByUuid(String uuid) {
2848		for (Conversation conversation : getConversations()) {
2849			if (conversation.getUuid().equals(uuid)) {
2850				return conversation;
2851			}
2852		}
2853		return null;
2854	}
2855
2856	public boolean markRead(final Conversation conversation) {
2857		mNotificationService.clear(conversation);
2858		final List<Message> readMessages = conversation.markRead();
2859		if (readMessages.size() > 0) {
2860			Runnable runnable = new Runnable() {
2861				@Override
2862				public void run() {
2863					for (Message message : readMessages) {
2864						databaseBackend.updateMessage(message);
2865					}
2866				}
2867			};
2868			mDatabaseExecutor.execute(runnable);
2869			updateUnreadCountBadge();
2870			return true;
2871		} else {
2872			return false;
2873		}
2874	}
2875
2876	public synchronized void updateUnreadCountBadge() {
2877		int count = unreadCount();
2878		if (unreadCount != count) {
2879			Log.d(Config.LOGTAG, "update unread count to " + count);
2880			if (count > 0) {
2881				ShortcutBadger.applyCount(getApplicationContext(), count);
2882			} else {
2883				ShortcutBadger.removeCount(getApplicationContext());
2884			}
2885			unreadCount = count;
2886		}
2887	}
2888
2889	public void sendReadMarker(final Conversation conversation) {
2890		final Message markable = conversation.getLatestMarkableMessage();
2891		if (this.markRead(conversation)) {
2892			updateConversationUi();
2893		}
2894		if (confirmMessages() && markable != null && markable.getRemoteMsgId() != null) {
2895			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
2896			Account account = conversation.getAccount();
2897			final Jid to = markable.getCounterpart();
2898			MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId());
2899			this.sendMessagePacket(conversation.getAccount(), packet);
2900		}
2901	}
2902
2903	public SecureRandom getRNG() {
2904		return this.mRandom;
2905	}
2906
2907	public MemorizingTrustManager getMemorizingTrustManager() {
2908		return this.mMemorizingTrustManager;
2909	}
2910
2911	public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
2912		this.mMemorizingTrustManager = trustManager;
2913	}
2914
2915	public void updateMemorizingTrustmanager() {
2916		final MemorizingTrustManager tm;
2917		final boolean dontTrustSystemCAs = getPreferences().getBoolean("dont_trust_system_cas", false);
2918		if (dontTrustSystemCAs) {
2919			tm = new MemorizingTrustManager(getApplicationContext(), null);
2920		} else {
2921			tm = new MemorizingTrustManager(getApplicationContext());
2922		}
2923		setMemorizingTrustManager(tm);
2924	}
2925
2926	public PowerManager getPowerManager() {
2927		return this.pm;
2928	}
2929
2930	public LruCache<String, Bitmap> getBitmapCache() {
2931		return this.mBitmapCache;
2932	}
2933
2934	public void syncRosterToDisk(final Account account) {
2935		Runnable runnable = new Runnable() {
2936
2937			@Override
2938			public void run() {
2939				databaseBackend.writeRoster(account.getRoster());
2940			}
2941		};
2942		mDatabaseExecutor.execute(runnable);
2943
2944	}
2945
2946	public List<String> getKnownHosts() {
2947		final List<String> hosts = new ArrayList<>();
2948		for (final Account account : getAccounts()) {
2949			if (!hosts.contains(account.getServer().toString())) {
2950				hosts.add(account.getServer().toString());
2951			}
2952			for (final Contact contact : account.getRoster().getContacts()) {
2953				if (contact.showInRoster()) {
2954					final String server = contact.getServer().toString();
2955					if (server != null && !hosts.contains(server)) {
2956						hosts.add(server);
2957					}
2958				}
2959			}
2960		}
2961		return hosts;
2962	}
2963
2964	public List<String> getKnownConferenceHosts() {
2965		final ArrayList<String> mucServers = new ArrayList<>();
2966		for (final Account account : accounts) {
2967			if (account.getXmppConnection() != null) {
2968				final String server = account.getXmppConnection().getMucServer();
2969				if (server != null && !mucServers.contains(server)) {
2970					mucServers.add(server);
2971				}
2972			}
2973		}
2974		return mucServers;
2975	}
2976
2977	public void sendMessagePacket(Account account, MessagePacket packet) {
2978		XmppConnection connection = account.getXmppConnection();
2979		if (connection != null) {
2980			connection.sendMessagePacket(packet);
2981		}
2982	}
2983
2984	public void sendPresencePacket(Account account, PresencePacket packet) {
2985		XmppConnection connection = account.getXmppConnection();
2986		if (connection != null) {
2987			connection.sendPresencePacket(packet);
2988		}
2989	}
2990
2991	public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
2992		XmppConnection connection = account.getXmppConnection();
2993		if (connection != null) {
2994			connection.sendCaptchaRegistryRequest(id, data);
2995		}
2996	}
2997
2998	public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
2999		final XmppConnection connection = account.getXmppConnection();
3000		if (connection != null) {
3001			connection.sendIqPacket(packet, callback);
3002		}
3003	}
3004
3005	public void sendPresence(final Account account) {
3006		PresencePacket packet;
3007		if (manuallyChangePresence()) {
3008			packet =  mPresenceGenerator.selfPresence(account, account.getPresenceStatus());
3009			String message = account.getPresenceStatusMessage();
3010			if (message != null && !message.isEmpty()) {
3011				packet.addChild(new Element("status").setContent(message));
3012			}
3013		} else {
3014			packet = mPresenceGenerator.selfPresence(account, getTargetPresence());
3015		}
3016		sendPresencePacket(account, packet);
3017	}
3018
3019	public void refreshAllPresences() {
3020		for (Account account : getAccounts()) {
3021			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
3022				sendPresence(account);
3023			}
3024		}
3025	}
3026
3027	private void refreshAllGcmTokens() {
3028		for(Account account : getAccounts()) {
3029			if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
3030				mPushManagementService.registerPushTokenOnServer(account);
3031			}
3032		}
3033	}
3034
3035	public void sendOfflinePresence(final Account account) {
3036		sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
3037	}
3038
3039	public MessageGenerator getMessageGenerator() {
3040		return this.mMessageGenerator;
3041	}
3042
3043	public PresenceGenerator getPresenceGenerator() {
3044		return this.mPresenceGenerator;
3045	}
3046
3047	public IqGenerator getIqGenerator() {
3048		return this.mIqGenerator;
3049	}
3050
3051	public IqParser getIqParser() {
3052		return this.mIqParser;
3053	}
3054
3055	public JingleConnectionManager getJingleConnectionManager() {
3056		return this.mJingleConnectionManager;
3057	}
3058
3059	public MessageArchiveService getMessageArchiveService() {
3060		return this.mMessageArchiveService;
3061	}
3062
3063	public List<Contact> findContacts(Jid jid) {
3064		ArrayList<Contact> contacts = new ArrayList<>();
3065		for (Account account : getAccounts()) {
3066			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
3067				Contact contact = account.getRoster().getContactFromRoster(jid);
3068				if (contact != null) {
3069					contacts.add(contact);
3070				}
3071			}
3072		}
3073		return contacts;
3074	}
3075
3076	public NotificationService getNotificationService() {
3077		return this.mNotificationService;
3078	}
3079
3080	public HttpConnectionManager getHttpConnectionManager() {
3081		return this.mHttpConnectionManager;
3082	}
3083
3084	public void resendFailedMessages(final Message message) {
3085		final Collection<Message> messages = new ArrayList<>();
3086		Message current = message;
3087		while (current.getStatus() == Message.STATUS_SEND_FAILED) {
3088			messages.add(current);
3089			if (current.mergeable(current.next())) {
3090				current = current.next();
3091			} else {
3092				break;
3093			}
3094		}
3095		for (final Message msg : messages) {
3096			msg.setTime(System.currentTimeMillis());
3097			markMessage(msg, Message.STATUS_WAITING);
3098			this.resendMessage(msg, false);
3099		}
3100	}
3101
3102	public void clearConversationHistory(final Conversation conversation) {
3103		conversation.clearMessages();
3104		conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam
3105		conversation.setLastClearHistory(System.currentTimeMillis());
3106		Runnable runnable = new Runnable() {
3107			@Override
3108			public void run() {
3109				databaseBackend.deleteMessagesInConversation(conversation);
3110			}
3111		};
3112		mDatabaseExecutor.execute(runnable);
3113	}
3114
3115	public void sendBlockRequest(final Blockable blockable) {
3116		if (blockable != null && blockable.getBlockedJid() != null) {
3117			final Jid jid = blockable.getBlockedJid();
3118			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid), new OnIqPacketReceived() {
3119
3120				@Override
3121				public void onIqPacketReceived(final Account account, final IqPacket packet) {
3122					if (packet.getType() == IqPacket.TYPE.RESULT) {
3123						account.getBlocklist().add(jid);
3124						updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
3125					}
3126				}
3127			});
3128		}
3129	}
3130
3131	public void sendUnblockRequest(final Blockable blockable) {
3132		if (blockable != null && blockable.getJid() != null) {
3133			final Jid jid = blockable.getBlockedJid();
3134			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() {
3135				@Override
3136				public void onIqPacketReceived(final Account account, final IqPacket packet) {
3137					if (packet.getType() == IqPacket.TYPE.RESULT) {
3138						account.getBlocklist().remove(jid);
3139						updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
3140					}
3141				}
3142			});
3143		}
3144	}
3145
3146	public void publishDisplayName(Account account) {
3147		String displayName = account.getDisplayName();
3148		if (displayName != null && !displayName.isEmpty()) {
3149			IqPacket publish = mIqGenerator.publishNick(displayName);
3150			sendIqPacket(account, publish, new OnIqPacketReceived() {
3151				@Override
3152				public void onIqPacketReceived(Account account, IqPacket packet) {
3153					if (packet.getType() == IqPacket.TYPE.ERROR) {
3154						Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not publish nick");
3155					}
3156				}
3157			});
3158		}
3159	}
3160
3161	private ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String,String> key) {
3162		ServiceDiscoveryResult result = discoCache.get(key);
3163		if (result != null) {
3164			return result;
3165		} else {
3166			result = databaseBackend.findDiscoveryResult(key.first, key.second);
3167			if (result != null) {
3168				discoCache.put(key, result);
3169			}
3170			return result;
3171		}
3172	}
3173
3174	public void fetchCaps(Account account, final Jid jid, final Presence presence) {
3175		final Pair<String,String> key = new Pair<>(presence.getHash(), presence.getVer());
3176		ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key);
3177		if (disco != null) {
3178			presence.setServiceDiscoveryResult(disco);
3179		} else {
3180			if (!account.inProgressDiscoFetches.contains(key)) {
3181				account.inProgressDiscoFetches.add(key);
3182				IqPacket request = new IqPacket(IqPacket.TYPE.GET);
3183				request.setTo(jid);
3184				request.query("http://jabber.org/protocol/disco#info");
3185				Log.d(Config.LOGTAG,account.getJid().toBareJid()+": making disco request for "+key.second+" to "+jid);
3186				sendIqPacket(account, request, new OnIqPacketReceived() {
3187					@Override
3188					public void onIqPacketReceived(Account account, IqPacket discoPacket) {
3189						if (discoPacket.getType() == IqPacket.TYPE.RESULT) {
3190							ServiceDiscoveryResult disco = new ServiceDiscoveryResult(discoPacket);
3191							if (presence.getVer().equals(disco.getVer())) {
3192								databaseBackend.insertDiscoveryResult(disco);
3193								injectServiceDiscorveryResult(account.getRoster(), presence.getHash(), presence.getVer(), disco);
3194							} else {
3195								Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + disco.getVer());
3196							}
3197						}
3198						account.inProgressDiscoFetches.remove(key);
3199					}
3200				});
3201			}
3202		}
3203	}
3204
3205	private void injectServiceDiscorveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) {
3206		for(Contact contact : roster.getContacts()) {
3207			for(Presence presence : contact.getPresences().getPresences().values()) {
3208				if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
3209					presence.setServiceDiscoveryResult(disco);
3210				}
3211			}
3212		}
3213	}
3214
3215	public void fetchMamPreferences(Account account, final OnMamPreferencesFetched callback) {
3216		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
3217		request.addChild("prefs","urn:xmpp:mam:0");
3218		sendIqPacket(account, request, new OnIqPacketReceived() {
3219			@Override
3220			public void onIqPacketReceived(Account account, IqPacket packet) {
3221				Element prefs = packet.findChild("prefs","urn:xmpp:mam:0");
3222				if (packet.getType() == IqPacket.TYPE.RESULT && prefs != null) {
3223					callback.onPreferencesFetched(prefs);
3224				} else {
3225					callback.onPreferencesFetchFailed();
3226				}
3227			}
3228		});
3229	}
3230
3231	public PushManagementService getPushManagementService() {
3232		return mPushManagementService;
3233	}
3234
3235	public Account getPendingAccount() {
3236		Account pending = null;
3237		for(Account account : getAccounts()) {
3238			if (account.isOptionSet(Account.OPTION_REGISTER)) {
3239				pending = account;
3240			} else {
3241				return null;
3242			}
3243		}
3244		return pending;
3245	}
3246
3247	public void changeStatus(Account account, Presence.Status status, String statusMessage) {
3248		databaseBackend.insertPresenceTemplate(new PresenceTemplate(status, statusMessage));
3249		changeStatusReal(account, status, statusMessage);
3250	}
3251
3252	private void changeStatusReal(Account account, Presence.Status status, String statusMessage) {
3253		account.setPresenceStatus(status);
3254		account.setPresenceStatusMessage(statusMessage);
3255		databaseBackend.updateAccount(account);
3256		if (!account.isOptionSet(Account.OPTION_DISABLED)) {
3257			sendPresence(account);
3258		}
3259	}
3260
3261	public void changeStatus(Presence.Status status, String statusMessage) {
3262		databaseBackend.insertPresenceTemplate(new PresenceTemplate(status, statusMessage));
3263		for(Account account : getAccounts()) {
3264			changeStatusReal(account, status, statusMessage);
3265		}
3266	}
3267
3268	public interface OnMamPreferencesFetched {
3269		void onPreferencesFetched(Element prefs);
3270		void onPreferencesFetchFailed();
3271	}
3272
3273	public void pushMamPreferences(Account account, Element prefs) {
3274		IqPacket set = new IqPacket(IqPacket.TYPE.SET);
3275		set.addChild(prefs);
3276		sendIqPacket(account, set, null);
3277	}
3278
3279	public interface OnAccountCreated {
3280		void onAccountCreated(Account account);
3281
3282		void informUser(int r);
3283	}
3284
3285	public interface OnMoreMessagesLoaded {
3286		void onMoreMessagesLoaded(int count, Conversation conversation);
3287
3288		void informUser(int r);
3289	}
3290
3291	public interface OnAccountPasswordChanged {
3292		void onPasswordChangeSucceeded();
3293
3294		void onPasswordChangeFailed();
3295	}
3296
3297	public interface OnAffiliationChanged {
3298		void onAffiliationChangedSuccessful(Jid jid);
3299
3300		void onAffiliationChangeFailed(Jid jid, int resId);
3301	}
3302
3303	public interface OnRoleChanged {
3304		void onRoleChangedSuccessful(String nick);
3305
3306		void onRoleChangeFailed(String nick, int resid);
3307	}
3308
3309	public interface OnConversationUpdate {
3310		void onConversationUpdate();
3311	}
3312
3313	public interface OnAccountUpdate {
3314		void onAccountUpdate();
3315	}
3316
3317	public interface OnCaptchaRequested {
3318		void onCaptchaRequested(Account account,
3319								String id,
3320								Data data,
3321								Bitmap captcha);
3322	}
3323
3324	public interface OnRosterUpdate {
3325		void onRosterUpdate();
3326	}
3327
3328	public interface OnMucRosterUpdate {
3329		void onMucRosterUpdate();
3330	}
3331
3332	public interface OnConferenceConfigurationFetched {
3333		void onConferenceConfigurationFetched(Conversation conversation);
3334
3335		void onFetchFailed(Conversation conversation, Element error);
3336	}
3337
3338	public interface OnConferenceJoined {
3339		void onConferenceJoined(Conversation conversation);
3340	}
3341
3342	public interface OnConferenceOptionsPushed {
3343		void onPushSucceeded();
3344
3345		void onPushFailed();
3346	}
3347
3348	public interface OnShowErrorToast {
3349		void onShowErrorToast(int resId);
3350	}
3351
3352	public class XmppConnectionBinder extends Binder {
3353		public XmppConnectionService getService() {
3354			return XmppConnectionService.this;
3355		}
3356	}
3357}