XmppConnectionService.java

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