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