XmppConnectionService.java

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