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