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		Collections.sort(list, new Comparator<Conversation>() {
1301			@Override
1302			public int compare(Conversation lhs, Conversation rhs) {
1303				Message left = lhs.getLatestMessage();
1304				Message right = rhs.getLatestMessage();
1305				if (left.getTimeSent() > right.getTimeSent()) {
1306					return -1;
1307				} else if (left.getTimeSent() < right.getTimeSent()) {
1308					return 1;
1309				} else {
1310					return 0;
1311				}
1312			}
1313		});
1314	}
1315
1316	public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
1317		if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback)) {
1318			return;
1319		} else if (timestamp == 0) {
1320			return;
1321		}
1322		Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp));
1323		Runnable runnable = new Runnable() {
1324			@Override
1325			public void run() {
1326				final Account account = conversation.getAccount();
1327				List<Message> messages = databaseBackend.getMessages(conversation, 50, timestamp);
1328				if (messages.size() > 0) {
1329					conversation.addAll(0, messages);
1330					checkDeletedFiles(conversation);
1331					callback.onMoreMessagesLoaded(messages.size(), conversation);
1332				} else if (conversation.hasMessagesLeftOnServer()
1333						&& account.isOnlineAndConnected()
1334						&& conversation.getLastClearHistory() == 0) {
1335					if ((conversation.getMode() == Conversation.MODE_SINGLE && account.getXmppConnection().getFeatures().mam())
1336							|| (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().mamSupport())) {
1337						MessageArchiveService.Query query = getMessageArchiveService().query(conversation, 0, timestamp);
1338						if (query != null) {
1339							query.setCallback(callback);
1340						}
1341						callback.informUser(R.string.fetching_history_from_server);
1342					}
1343				}
1344			}
1345		};
1346		mDatabaseExecutor.execute(runnable);
1347	}
1348
1349	public List<Account> getAccounts() {
1350		return this.accounts;
1351	}
1352
1353	public Conversation find(final Iterable<Conversation> haystack, final Contact contact) {
1354		for (final Conversation conversation : haystack) {
1355			if (conversation.getContact() == contact) {
1356				return conversation;
1357			}
1358		}
1359		return null;
1360	}
1361
1362	public Conversation find(final Iterable<Conversation> haystack, final Account account, final Jid jid) {
1363		if (jid == null) {
1364			return null;
1365		}
1366		for (final Conversation conversation : haystack) {
1367			if ((account == null || conversation.getAccount() == account)
1368					&& (conversation.getJid().toBareJid().equals(jid.toBareJid()))) {
1369				return conversation;
1370			}
1371		}
1372		return null;
1373	}
1374
1375	public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc) {
1376		return this.findOrCreateConversation(account, jid, muc, null);
1377	}
1378
1379	public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final MessageArchiveService.Query query) {
1380		synchronized (this.conversations) {
1381			Conversation conversation = find(account, jid);
1382			if (conversation != null) {
1383				return conversation;
1384			}
1385			conversation = databaseBackend.findConversation(account, jid);
1386			if (conversation != null) {
1387				conversation.setStatus(Conversation.STATUS_AVAILABLE);
1388				conversation.setAccount(account);
1389				if (muc) {
1390					conversation.setMode(Conversation.MODE_MULTI);
1391					conversation.setContactJid(jid);
1392				} else {
1393					conversation.setMode(Conversation.MODE_SINGLE);
1394					conversation.setContactJid(jid.toBareJid());
1395				}
1396				conversation.setNextEncryption(-1);
1397				conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
1398				this.databaseBackend.updateConversation(conversation);
1399			} else {
1400				String conversationName;
1401				Contact contact = account.getRoster().getContact(jid);
1402				if (contact != null) {
1403					conversationName = contact.getDisplayName();
1404				} else {
1405					conversationName = jid.getLocalpart();
1406				}
1407				if (muc) {
1408					conversation = new Conversation(conversationName, account, jid,
1409							Conversation.MODE_MULTI);
1410				} else {
1411					conversation = new Conversation(conversationName, account, jid.toBareJid(),
1412							Conversation.MODE_SINGLE);
1413				}
1414				this.databaseBackend.createConversation(conversation);
1415			}
1416			if (account.getXmppConnection() != null
1417					&& account.getXmppConnection().getFeatures().mam()
1418					&& !muc) {
1419				if (query == null) {
1420					this.mMessageArchiveService.query(conversation);
1421				} else {
1422					if (query.getConversation() == null) {
1423						this.mMessageArchiveService.query(conversation, query.getStart());
1424					}
1425				}
1426			}
1427			checkDeletedFiles(conversation);
1428			this.conversations.add(conversation);
1429			updateConversationUi();
1430			return conversation;
1431		}
1432	}
1433
1434	public void archiveConversation(Conversation conversation) {
1435		getNotificationService().clear(conversation);
1436		conversation.setStatus(Conversation.STATUS_ARCHIVED);
1437		conversation.setNextEncryption(-1);
1438		synchronized (this.conversations) {
1439			if (conversation.getMode() == Conversation.MODE_MULTI) {
1440				if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1441					Bookmark bookmark = conversation.getBookmark();
1442					if (bookmark != null && bookmark.autojoin() && respectAutojoin()) {
1443						bookmark.setAutojoin(false);
1444						pushBookmarks(bookmark.getAccount());
1445					}
1446				}
1447				leaveMuc(conversation);
1448			} else {
1449				conversation.endOtrIfNeeded();
1450				if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1451					Log.d(Config.LOGTAG, "Canceling presence request from " + conversation.getJid().toString());
1452					sendPresencePacket(
1453							conversation.getAccount(),
1454							mPresenceGenerator.stopPresenceUpdatesTo(conversation.getContact())
1455					);
1456				}
1457			}
1458			this.databaseBackend.updateConversation(conversation);
1459			this.conversations.remove(conversation);
1460			updateConversationUi();
1461		}
1462	}
1463
1464	public void createAccount(final Account account) {
1465		account.initAccountServices(this);
1466		databaseBackend.createAccount(account);
1467		this.accounts.add(account);
1468		this.reconnectAccountInBackground(account);
1469		updateAccountUi();
1470	}
1471
1472	public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
1473		new Thread(new Runnable() {
1474			@Override
1475			public void run() {
1476				try {
1477					X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
1478					Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
1479					if (findAccountByJid(info.first) == null) {
1480						Account account = new Account(info.first, "");
1481						account.setPrivateKeyAlias(alias);
1482						account.setOption(Account.OPTION_DISABLED, true);
1483						account.setDisplayName(info.second);
1484						createAccount(account);
1485						callback.onAccountCreated(account);
1486						if (Config.X509_VERIFICATION) {
1487							try {
1488								getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
1489							} catch (CertificateException e) {
1490								callback.informUser(R.string.certificate_chain_is_not_trusted);
1491							}
1492						}
1493					} else {
1494						callback.informUser(R.string.account_already_exists);
1495					}
1496				} catch (Exception e) {
1497					e.printStackTrace();
1498					callback.informUser(R.string.unable_to_parse_certificate);
1499				}
1500			}
1501		}).start();
1502
1503	}
1504
1505	public void updateKeyInAccount(final Account account, final String alias) {
1506		Log.d(Config.LOGTAG, "update key in account " + alias);
1507		try {
1508			X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
1509			Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
1510			if (account.getJid().toBareJid().equals(info.first)) {
1511				account.setPrivateKeyAlias(alias);
1512				account.setDisplayName(info.second);
1513				databaseBackend.updateAccount(account);
1514				if (Config.X509_VERIFICATION) {
1515					try {
1516						getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
1517					} catch (CertificateException e) {
1518						showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
1519					}
1520					account.getAxolotlService().regenerateKeys(true);
1521				}
1522			} else {
1523				showErrorToastInUi(R.string.jid_does_not_match_certificate);
1524			}
1525		} catch (Exception e) {
1526			e.printStackTrace();
1527		}
1528	}
1529
1530	public void updateAccount(final Account account) {
1531		this.statusListener.onStatusChanged(account);
1532		databaseBackend.updateAccount(account);
1533		reconnectAccountInBackground(account);
1534		updateAccountUi();
1535		getNotificationService().updateErrorNotification();
1536	}
1537
1538	public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) {
1539		final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword);
1540		sendIqPacket(account, iq, new OnIqPacketReceived() {
1541			@Override
1542			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1543				if (packet.getType() == IqPacket.TYPE.RESULT) {
1544					account.setPassword(newPassword);
1545					account.setOption(Account.OPTION_MAGIC_CREATE, false);
1546					databaseBackend.updateAccount(account);
1547					callback.onPasswordChangeSucceeded();
1548				} else {
1549					callback.onPasswordChangeFailed();
1550				}
1551			}
1552		});
1553	}
1554
1555	public void deleteAccount(final Account account) {
1556		synchronized (this.conversations) {
1557			for (final Conversation conversation : conversations) {
1558				if (conversation.getAccount() == account) {
1559					if (conversation.getMode() == Conversation.MODE_MULTI) {
1560						leaveMuc(conversation);
1561					} else if (conversation.getMode() == Conversation.MODE_SINGLE) {
1562						conversation.endOtrIfNeeded();
1563					}
1564					conversations.remove(conversation);
1565				}
1566			}
1567			if (account.getXmppConnection() != null) {
1568				new Thread(new Runnable() {
1569					@Override
1570					public void run() {
1571						disconnect(account, true);
1572					}
1573				});
1574			}
1575			Runnable runnable = new Runnable() {
1576				@Override
1577				public void run() {
1578					databaseBackend.deleteAccount(account);
1579				}
1580			};
1581			mDatabaseExecutor.execute(runnable);
1582			this.accounts.remove(account);
1583			updateAccountUi();
1584			getNotificationService().updateErrorNotification();
1585		}
1586	}
1587
1588	public void setOnConversationListChangedListener(OnConversationUpdate listener) {
1589		synchronized (this) {
1590			this.mLastActivity = System.currentTimeMillis();
1591			if (checkListeners()) {
1592				switchToForeground();
1593			}
1594			this.mOnConversationUpdate = listener;
1595			this.mNotificationService.setIsInForeground(true);
1596			if (this.convChangedListenerCount < 2) {
1597				this.convChangedListenerCount++;
1598			}
1599		}
1600	}
1601
1602	public void removeOnConversationListChangedListener() {
1603		synchronized (this) {
1604			this.convChangedListenerCount--;
1605			if (this.convChangedListenerCount <= 0) {
1606				this.convChangedListenerCount = 0;
1607				this.mOnConversationUpdate = null;
1608				this.mNotificationService.setIsInForeground(false);
1609				if (checkListeners()) {
1610					switchToBackground();
1611				}
1612			}
1613		}
1614	}
1615
1616	public void setOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
1617		synchronized (this) {
1618			if (checkListeners()) {
1619				switchToForeground();
1620			}
1621			this.mOnShowErrorToast = onShowErrorToast;
1622			if (this.showErrorToastListenerCount < 2) {
1623				this.showErrorToastListenerCount++;
1624			}
1625		}
1626		this.mOnShowErrorToast = onShowErrorToast;
1627	}
1628
1629	public void removeOnShowErrorToastListener() {
1630		synchronized (this) {
1631			this.showErrorToastListenerCount--;
1632			if (this.showErrorToastListenerCount <= 0) {
1633				this.showErrorToastListenerCount = 0;
1634				this.mOnShowErrorToast = null;
1635				if (checkListeners()) {
1636					switchToBackground();
1637				}
1638			}
1639		}
1640	}
1641
1642	public void setOnAccountListChangedListener(OnAccountUpdate listener) {
1643		synchronized (this) {
1644			if (checkListeners()) {
1645				switchToForeground();
1646			}
1647			this.mOnAccountUpdate = listener;
1648			if (this.accountChangedListenerCount < 2) {
1649				this.accountChangedListenerCount++;
1650			}
1651		}
1652	}
1653
1654	public void removeOnAccountListChangedListener() {
1655		synchronized (this) {
1656			this.accountChangedListenerCount--;
1657			if (this.accountChangedListenerCount <= 0) {
1658				this.mOnAccountUpdate = null;
1659				this.accountChangedListenerCount = 0;
1660				if (checkListeners()) {
1661					switchToBackground();
1662				}
1663			}
1664		}
1665	}
1666
1667	public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
1668		synchronized (this) {
1669			if (checkListeners()) {
1670				switchToForeground();
1671			}
1672			this.mOnCaptchaRequested = listener;
1673			if (this.captchaRequestedListenerCount < 2) {
1674				this.captchaRequestedListenerCount++;
1675			}
1676		}
1677	}
1678
1679	public void removeOnCaptchaRequestedListener() {
1680		synchronized (this) {
1681			this.captchaRequestedListenerCount--;
1682			if (this.captchaRequestedListenerCount <= 0) {
1683				this.mOnCaptchaRequested = null;
1684				this.captchaRequestedListenerCount = 0;
1685				if (checkListeners()) {
1686					switchToBackground();
1687				}
1688			}
1689		}
1690	}
1691
1692	public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
1693		synchronized (this) {
1694			if (checkListeners()) {
1695				switchToForeground();
1696			}
1697			this.mOnRosterUpdate = listener;
1698			if (this.rosterChangedListenerCount < 2) {
1699				this.rosterChangedListenerCount++;
1700			}
1701		}
1702	}
1703
1704	public void removeOnRosterUpdateListener() {
1705		synchronized (this) {
1706			this.rosterChangedListenerCount--;
1707			if (this.rosterChangedListenerCount <= 0) {
1708				this.rosterChangedListenerCount = 0;
1709				this.mOnRosterUpdate = null;
1710				if (checkListeners()) {
1711					switchToBackground();
1712				}
1713			}
1714		}
1715	}
1716
1717	public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
1718		synchronized (this) {
1719			if (checkListeners()) {
1720				switchToForeground();
1721			}
1722			this.mOnUpdateBlocklist = listener;
1723			if (this.updateBlocklistListenerCount < 2) {
1724				this.updateBlocklistListenerCount++;
1725			}
1726		}
1727	}
1728
1729	public void removeOnUpdateBlocklistListener() {
1730		synchronized (this) {
1731			this.updateBlocklistListenerCount--;
1732			if (this.updateBlocklistListenerCount <= 0) {
1733				this.updateBlocklistListenerCount = 0;
1734				this.mOnUpdateBlocklist = null;
1735				if (checkListeners()) {
1736					switchToBackground();
1737				}
1738			}
1739		}
1740	}
1741
1742	public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
1743		synchronized (this) {
1744			if (checkListeners()) {
1745				switchToForeground();
1746			}
1747			this.mOnKeyStatusUpdated = listener;
1748			if (this.keyStatusUpdatedListenerCount < 2) {
1749				this.keyStatusUpdatedListenerCount++;
1750			}
1751		}
1752	}
1753
1754	public void removeOnNewKeysAvailableListener() {
1755		synchronized (this) {
1756			this.keyStatusUpdatedListenerCount--;
1757			if (this.keyStatusUpdatedListenerCount <= 0) {
1758				this.keyStatusUpdatedListenerCount = 0;
1759				this.mOnKeyStatusUpdated = null;
1760				if (checkListeners()) {
1761					switchToBackground();
1762				}
1763			}
1764		}
1765	}
1766
1767	public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
1768		synchronized (this) {
1769			if (checkListeners()) {
1770				switchToForeground();
1771			}
1772			this.mOnMucRosterUpdate = listener;
1773			if (this.mucRosterChangedListenerCount < 2) {
1774				this.mucRosterChangedListenerCount++;
1775			}
1776		}
1777	}
1778
1779	public void removeOnMucRosterUpdateListener() {
1780		synchronized (this) {
1781			this.mucRosterChangedListenerCount--;
1782			if (this.mucRosterChangedListenerCount <= 0) {
1783				this.mucRosterChangedListenerCount = 0;
1784				this.mOnMucRosterUpdate = null;
1785				if (checkListeners()) {
1786					switchToBackground();
1787				}
1788			}
1789		}
1790	}
1791
1792	public boolean checkListeners() {
1793		return (this.mOnAccountUpdate == null
1794				&& this.mOnConversationUpdate == null
1795				&& this.mOnRosterUpdate == null
1796				&& this.mOnCaptchaRequested == null
1797				&& this.mOnUpdateBlocklist == null
1798				&& this.mOnShowErrorToast == null
1799				&& this.mOnKeyStatusUpdated == null);
1800	}
1801
1802	private void switchToForeground() {
1803		final boolean broadcastLastActivity = broadcastLastActivity();
1804		for (Conversation conversation : getConversations()) {
1805			conversation.setIncomingChatState(ChatState.ACTIVE);
1806		}
1807		for (Account account : getAccounts()) {
1808			if (account.getStatus() == Account.State.ONLINE) {
1809				account.deactivateGracePeriod();
1810				final XmppConnection connection = account.getXmppConnection();
1811				if (connection != null ) {
1812					if (connection.getFeatures().csi()) {
1813						connection.sendActive();
1814					}
1815					if (broadcastLastActivity) {
1816						sendPresence(account, false); //send new presence but don't include idle because we are not
1817					}
1818				}
1819			}
1820		}
1821		Log.d(Config.LOGTAG, "app switched into foreground");
1822	}
1823
1824	private void switchToBackground() {
1825		final boolean broadcastLastActivity = broadcastLastActivity();
1826		for (Account account : getAccounts()) {
1827			if (account.getStatus() == Account.State.ONLINE) {
1828				XmppConnection connection = account.getXmppConnection();
1829				if (connection != null) {
1830					if (broadcastLastActivity) {
1831						sendPresence(account, broadcastLastActivity);
1832					}
1833					if (connection.getFeatures().csi()) {
1834						connection.sendInactive();
1835					}
1836					if (Config.CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND && mPushManagementService.available(account)) {
1837						connection.waitForPush();
1838						cancelWakeUpCall(account.getUuid().hashCode());
1839					}
1840				}
1841			}
1842		}
1843		this.mNotificationService.setIsInForeground(false);
1844		Log.d(Config.LOGTAG, "app switched into background");
1845	}
1846
1847	private void connectMultiModeConversations(Account account) {
1848		List<Conversation> conversations = getConversations();
1849		for (Conversation conversation : conversations) {
1850			if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) {
1851				joinMuc(conversation);
1852			}
1853		}
1854	}
1855
1856	public void joinMuc(Conversation conversation) {
1857		joinMuc(conversation, null);
1858	}
1859
1860	private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
1861		Account account = conversation.getAccount();
1862		account.pendingConferenceJoins.remove(conversation);
1863		account.pendingConferenceLeaves.remove(conversation);
1864		if (account.getStatus() == Account.State.ONLINE) {
1865			conversation.resetMucOptions();
1866			if (onConferenceJoined != null) {
1867				conversation.getMucOptions().flagNoAutoPushConfiguration();
1868			}
1869			conversation.setHasMessagesLeftOnServer(false);
1870			fetchConferenceConfiguration(conversation, new OnConferenceConfigurationFetched() {
1871
1872				private void join(Conversation conversation) {
1873					Account account = conversation.getAccount();
1874					final MucOptions mucOptions = conversation.getMucOptions();
1875					final Jid joinJid = mucOptions.getSelf().getFullJid();
1876					Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString());
1877					PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE);
1878					packet.setTo(joinJid);
1879					Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
1880					if (conversation.getMucOptions().getPassword() != null) {
1881						x.addChild("password").setContent(conversation.getMucOptions().getPassword());
1882					}
1883
1884					if (mucOptions.mamSupport()) {
1885						// Use MAM instead of the limited muc history to get history
1886						x.addChild("history").setAttribute("maxchars", "0");
1887					} else {
1888						// Fallback to muc history
1889						x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted()));
1890					}
1891					sendPresencePacket(account, packet);
1892					if (onConferenceJoined != null) {
1893						onConferenceJoined.onConferenceJoined(conversation);
1894					}
1895					if (!joinJid.equals(conversation.getJid())) {
1896						conversation.setContactJid(joinJid);
1897						databaseBackend.updateConversation(conversation);
1898					}
1899
1900					if (mucOptions.mamSupport()) {
1901						getMessageArchiveService().catchupMUC(conversation);
1902					}
1903					if (mucOptions.membersOnly() && mucOptions.nonanonymous()) {
1904						fetchConferenceMembers(conversation);
1905					}
1906					sendUnsentMessages(conversation);
1907				}
1908
1909				@Override
1910				public void onConferenceConfigurationFetched(Conversation conversation) {
1911					join(conversation);
1912				}
1913
1914				@Override
1915				public void onFetchFailed(final Conversation conversation, Element error) {
1916					join(conversation);
1917					fetchConferenceConfiguration(conversation);
1918				}
1919			});
1920			updateConversationUi();
1921		} else {
1922			account.pendingConferenceJoins.add(conversation);
1923			conversation.resetMucOptions();
1924			conversation.setHasMessagesLeftOnServer(false);
1925			updateConversationUi();
1926		}
1927	}
1928
1929	private void fetchConferenceMembers(final Conversation conversation) {
1930		final Account account = conversation.getAccount();
1931		final String[] affiliations = {"member","admin","owner"};
1932		OnIqPacketReceived callback = new OnIqPacketReceived() {
1933
1934			private int i = 0;
1935
1936			@Override
1937			public void onIqPacketReceived(Account account, IqPacket packet) {
1938
1939				Element query = packet.query("http://jabber.org/protocol/muc#admin");
1940				if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
1941					for(Element child : query.getChildren()) {
1942						if ("item".equals(child.getName())) {
1943							MucOptions.User user = AbstractParser.parseItem(conversation,child);
1944							if (!user.realJidMatchesAccount()) {
1945								conversation.getMucOptions().addUser(user);
1946								getAvatarService().clear(conversation);
1947								updateMucRosterUi();
1948								updateConversationUi();
1949							}
1950						}
1951					}
1952				} else {
1953					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not request affiliation "+affiliations[i]+" in "+conversation.getJid().toBareJid());
1954				}
1955				++i;
1956				if (i >= affiliations.length) {
1957					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved members for "+conversation.getJid().toBareJid()+": "+conversation.getMucOptions().getMembers());
1958				}
1959			}
1960		};
1961		for(String affiliation : affiliations) {
1962			sendIqPacket(account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
1963		}
1964		Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetching members for "+conversation.getName());
1965	}
1966
1967	public void providePasswordForMuc(Conversation conversation, String password) {
1968		if (conversation.getMode() == Conversation.MODE_MULTI) {
1969			conversation.getMucOptions().setPassword(password);
1970			if (conversation.getBookmark() != null) {
1971				if (respectAutojoin()) {
1972					conversation.getBookmark().setAutojoin(true);
1973				}
1974				pushBookmarks(conversation.getAccount());
1975			}
1976			databaseBackend.updateConversation(conversation);
1977			joinMuc(conversation);
1978		}
1979	}
1980
1981	public void renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
1982		final MucOptions options = conversation.getMucOptions();
1983		final Jid joinJid = options.createJoinJid(nick);
1984		if (options.online()) {
1985			Account account = conversation.getAccount();
1986			options.setOnRenameListener(new OnRenameListener() {
1987
1988				@Override
1989				public void onSuccess() {
1990					conversation.setContactJid(joinJid);
1991					databaseBackend.updateConversation(conversation);
1992					Bookmark bookmark = conversation.getBookmark();
1993					if (bookmark != null) {
1994						bookmark.setNick(nick);
1995						pushBookmarks(bookmark.getAccount());
1996					}
1997					callback.success(conversation);
1998				}
1999
2000				@Override
2001				public void onFailure() {
2002					callback.error(R.string.nick_in_use, conversation);
2003				}
2004			});
2005
2006			PresencePacket packet = new PresencePacket();
2007			packet.setTo(joinJid);
2008			packet.setFrom(conversation.getAccount().getJid());
2009
2010			String sig = account.getPgpSignature();
2011			if (sig != null) {
2012				packet.addChild("status").setContent("online");
2013				packet.addChild("x", "jabber:x:signed").setContent(sig);
2014			}
2015			sendPresencePacket(account, packet);
2016		} else {
2017			conversation.setContactJid(joinJid);
2018			databaseBackend.updateConversation(conversation);
2019			if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
2020				Bookmark bookmark = conversation.getBookmark();
2021				if (bookmark != null) {
2022					bookmark.setNick(nick);
2023					pushBookmarks(bookmark.getAccount());
2024				}
2025				joinMuc(conversation);
2026			}
2027		}
2028	}
2029
2030	public void leaveMuc(Conversation conversation) {
2031		leaveMuc(conversation, false);
2032	}
2033
2034	private void leaveMuc(Conversation conversation, boolean now) {
2035		Account account = conversation.getAccount();
2036		account.pendingConferenceJoins.remove(conversation);
2037		account.pendingConferenceLeaves.remove(conversation);
2038		if (account.getStatus() == Account.State.ONLINE || now) {
2039			PresencePacket packet = new PresencePacket();
2040			packet.setTo(conversation.getMucOptions().getSelf().getFullJid());
2041			packet.setFrom(conversation.getAccount().getJid());
2042			packet.setAttribute("type", "unavailable");
2043			sendPresencePacket(conversation.getAccount(), packet);
2044			conversation.getMucOptions().setOffline();
2045			conversation.deregisterWithBookmark();
2046			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
2047					+ ": leaving muc " + conversation.getJid());
2048		} else {
2049			account.pendingConferenceLeaves.add(conversation);
2050		}
2051	}
2052
2053	private String findConferenceServer(final Account account) {
2054		String server;
2055		if (account.getXmppConnection() != null) {
2056			server = account.getXmppConnection().getMucServer();
2057			if (server != null) {
2058				return server;
2059			}
2060		}
2061		for (Account other : getAccounts()) {
2062			if (other != account && other.getXmppConnection() != null) {
2063				server = other.getXmppConnection().getMucServer();
2064				if (server != null) {
2065					return server;
2066				}
2067			}
2068		}
2069		return null;
2070	}
2071
2072	public void createAdhocConference(final Account account,
2073									  final String subject,
2074									  final Iterable<Jid> jids,
2075									  final UiCallback<Conversation> callback) {
2076		Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString());
2077		if (account.getStatus() == Account.State.ONLINE) {
2078			try {
2079				String server = findConferenceServer(account);
2080				if (server == null) {
2081					if (callback != null) {
2082						callback.error(R.string.no_conference_server_found, null);
2083					}
2084					return;
2085				}
2086				final Jid jid = Jid.fromParts(new BigInteger(64, getRNG()).toString(Character.MAX_RADIX), server, null);
2087				final Conversation conversation = findOrCreateConversation(account, jid, true);
2088				joinMuc(conversation, new OnConferenceJoined() {
2089					@Override
2090					public void onConferenceJoined(final Conversation conversation) {
2091						pushConferenceConfiguration(conversation, IqGenerator.defaultRoomConfiguration(), new OnConferenceOptionsPushed() {
2092							@Override
2093							public void onPushSucceeded() {
2094								if (subject != null && !subject.trim().isEmpty()) {
2095									pushSubjectToConference(conversation, subject.trim());
2096								}
2097								for (Jid invite : jids) {
2098									invite(conversation, invite);
2099								}
2100								if (account.countPresences() > 1) {
2101									directInvite(conversation, account.getJid().toBareJid());
2102								}
2103								saveConversationAsBookmark(conversation, subject);
2104								if (callback != null) {
2105									callback.success(conversation);
2106								}
2107							}
2108
2109							@Override
2110							public void onPushFailed() {
2111								archiveConversation(conversation);
2112								if (callback != null) {
2113									callback.error(R.string.conference_creation_failed, conversation);
2114								}
2115							}
2116						});
2117					}
2118				});
2119			} catch (InvalidJidException e) {
2120				if (callback != null) {
2121					callback.error(R.string.conference_creation_failed, null);
2122				}
2123			}
2124		} else {
2125			if (callback != null) {
2126				callback.error(R.string.not_connected_try_again, null);
2127			}
2128		}
2129	}
2130
2131	public void fetchConferenceConfiguration(final Conversation conversation) {
2132		fetchConferenceConfiguration(conversation, null);
2133	}
2134
2135	public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) {
2136		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2137		request.setTo(conversation.getJid().toBareJid());
2138		request.query("http://jabber.org/protocol/disco#info");
2139		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
2140			@Override
2141			public void onIqPacketReceived(Account account, IqPacket packet) {
2142				Element query = packet.findChild("query","http://jabber.org/protocol/disco#info");
2143				if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
2144					ArrayList<String> features = new ArrayList<>();
2145					for (Element child : query.getChildren()) {
2146						if (child != null && child.getName().equals("feature")) {
2147							String var = child.getAttribute("var");
2148							if (var != null) {
2149								features.add(var);
2150							}
2151						}
2152					}
2153					Element form = query.findChild("x", "jabber:x:data");
2154					if (form != null) {
2155						conversation.getMucOptions().updateFormData(Data.parse(form));
2156					}
2157					conversation.getMucOptions().updateFeatures(features);
2158					if (callback != null) {
2159						callback.onConferenceConfigurationFetched(conversation);
2160					}
2161					Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetched muc configuration for "+conversation.getJid().toBareJid()+" - "+features.toString());
2162					updateConversationUi();
2163				} else if (packet.getType() == IqPacket.TYPE.ERROR) {
2164					if (callback != null) {
2165						callback.onFetchFailed(conversation, packet.getError());
2166					}
2167				}
2168			}
2169		});
2170	}
2171
2172	public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConferenceOptionsPushed callback) {
2173		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2174		request.setTo(conversation.getJid().toBareJid());
2175		request.query("http://jabber.org/protocol/muc#owner");
2176		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
2177			@Override
2178			public void onIqPacketReceived(Account account, IqPacket packet) {
2179				if (packet.getType() == IqPacket.TYPE.RESULT) {
2180					Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
2181					for (Field field : data.getFields()) {
2182						if (options.containsKey(field.getFieldName())) {
2183							field.setValue(options.getString(field.getFieldName()));
2184						}
2185					}
2186					data.submit();
2187					IqPacket set = new IqPacket(IqPacket.TYPE.SET);
2188					set.setTo(conversation.getJid().toBareJid());
2189					set.query("http://jabber.org/protocol/muc#owner").addChild(data);
2190					sendIqPacket(account, set, new OnIqPacketReceived() {
2191						@Override
2192						public void onIqPacketReceived(Account account, IqPacket packet) {
2193							if (callback != null) {
2194								if (packet.getType() == IqPacket.TYPE.RESULT) {
2195									callback.onPushSucceeded();
2196								} else {
2197									callback.onPushFailed();
2198								}
2199							}
2200						}
2201					});
2202				} else {
2203					if (callback != null) {
2204						callback.onPushFailed();
2205					}
2206				}
2207			}
2208		});
2209	}
2210
2211	public void pushSubjectToConference(final Conversation conference, final String subject) {
2212		MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, subject);
2213		this.sendMessagePacket(conference.getAccount(), packet);
2214		final MucOptions mucOptions = conference.getMucOptions();
2215		final MucOptions.User self = mucOptions.getSelf();
2216		if (!mucOptions.persistent() && self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
2217			Bundle options = new Bundle();
2218			options.putString("muc#roomconfig_persistentroom", "1");
2219			this.pushConferenceConfiguration(conference, options, null);
2220		}
2221	}
2222
2223	public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
2224		final Jid jid = user.toBareJid();
2225		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
2226		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
2227			@Override
2228			public void onIqPacketReceived(Account account, IqPacket packet) {
2229				if (packet.getType() == IqPacket.TYPE.RESULT) {
2230					conference.getMucOptions().changeAffiliation(jid, affiliation);
2231					getAvatarService().clear(conference);
2232					callback.onAffiliationChangedSuccessful(jid);
2233				} else {
2234					callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
2235				}
2236			}
2237		});
2238	}
2239
2240	public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
2241		List<Jid> jids = new ArrayList<>();
2242		for (MucOptions.User user : conference.getMucOptions().getUsers()) {
2243			if (user.getAffiliation() == before && user.getRealJid() != null) {
2244				jids.add(user.getRealJid());
2245			}
2246		}
2247		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
2248		sendIqPacket(conference.getAccount(), request, mDefaultIqHandler);
2249	}
2250
2251	public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) {
2252		IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
2253		Log.d(Config.LOGTAG, request.toString());
2254		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
2255			@Override
2256			public void onIqPacketReceived(Account account, IqPacket packet) {
2257				Log.d(Config.LOGTAG, packet.toString());
2258				if (packet.getType() == IqPacket.TYPE.RESULT) {
2259					callback.onRoleChangedSuccessful(nick);
2260				} else {
2261					callback.onRoleChangeFailed(nick, R.string.could_not_change_role);
2262				}
2263			}
2264		});
2265	}
2266
2267	private void disconnect(Account account, boolean force) {
2268		if ((account.getStatus() == Account.State.ONLINE)
2269				|| (account.getStatus() == Account.State.DISABLED)) {
2270			final XmppConnection connection = account.getXmppConnection();
2271			if (!force) {
2272				List<Conversation> conversations = getConversations();
2273				for (Conversation conversation : conversations) {
2274					if (conversation.getAccount() == account) {
2275						if (conversation.getMode() == Conversation.MODE_MULTI) {
2276							leaveMuc(conversation, true);
2277						} else {
2278							if (conversation.endOtrIfNeeded()) {
2279								Log.d(Config.LOGTAG, account.getJid().toBareJid()
2280										+ ": ended otr session with "
2281										+ conversation.getJid());
2282							}
2283						}
2284					}
2285				}
2286				sendOfflinePresence(account);
2287			}
2288			connection.disconnect(force);
2289		}
2290	}
2291
2292	@Override
2293	public IBinder onBind(Intent intent) {
2294		return mBinder;
2295	}
2296
2297	public void updateMessage(Message message) {
2298		databaseBackend.updateMessage(message);
2299		updateConversationUi();
2300	}
2301
2302	public void updateMessage(Message message, String uuid) {
2303		databaseBackend.updateMessage(message, uuid);
2304		updateConversationUi();
2305	}
2306
2307	protected void syncDirtyContacts(Account account) {
2308		for (Contact contact : account.getRoster().getContacts()) {
2309			if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
2310				pushContactToServer(contact);
2311			}
2312			if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
2313				deleteContactOnServer(contact);
2314			}
2315		}
2316	}
2317
2318	public void createContact(Contact contact) {
2319		boolean autoGrant = getPreferences().getBoolean("grant_new_contacts", true);
2320		if (autoGrant) {
2321			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
2322			contact.setOption(Contact.Options.ASKING);
2323		}
2324		pushContactToServer(contact);
2325	}
2326
2327	public void onOtrSessionEstablished(Conversation conversation) {
2328		final Account account = conversation.getAccount();
2329		final Session otrSession = conversation.getOtrSession();
2330		Log.d(Config.LOGTAG,
2331				account.getJid().toBareJid() + " otr session established with "
2332						+ conversation.getJid() + "/"
2333						+ otrSession.getSessionID().getUserID());
2334		conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
2335
2336			@Override
2337			public void onMessageFound(Message message) {
2338				SessionID id = otrSession.getSessionID();
2339				try {
2340					message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
2341				} catch (InvalidJidException e) {
2342					return;
2343				}
2344				if (message.needsUploading()) {
2345					mJingleConnectionManager.createNewConnection(message);
2346				} else {
2347					MessagePacket outPacket = mMessageGenerator.generateOtrChat(message);
2348					if (outPacket != null) {
2349						mMessageGenerator.addDelay(outPacket, message.getTimeSent());
2350						message.setStatus(Message.STATUS_SEND);
2351						databaseBackend.updateMessage(message);
2352						sendMessagePacket(account, outPacket);
2353					}
2354				}
2355				updateConversationUi();
2356			}
2357		});
2358	}
2359
2360	public boolean renewSymmetricKey(Conversation conversation) {
2361		Account account = conversation.getAccount();
2362		byte[] symmetricKey = new byte[32];
2363		this.mRandom.nextBytes(symmetricKey);
2364		Session otrSession = conversation.getOtrSession();
2365		if (otrSession != null) {
2366			MessagePacket packet = new MessagePacket();
2367			packet.setType(MessagePacket.TYPE_CHAT);
2368			packet.setFrom(account.getJid());
2369			MessageGenerator.addMessageHints(packet);
2370			packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
2371					+ otrSession.getSessionID().getUserID());
2372			try {
2373				packet.setBody(otrSession
2374						.transformSending(CryptoHelper.FILETRANSFER
2375								+ CryptoHelper.bytesToHex(symmetricKey))[0]);
2376				sendMessagePacket(account, packet);
2377				conversation.setSymmetricKey(symmetricKey);
2378				return true;
2379			} catch (OtrException e) {
2380				return false;
2381			}
2382		}
2383		return false;
2384	}
2385
2386	public void pushContactToServer(final Contact contact) {
2387		contact.resetOption(Contact.Options.DIRTY_DELETE);
2388		contact.setOption(Contact.Options.DIRTY_PUSH);
2389		final Account account = contact.getAccount();
2390		if (account.getStatus() == Account.State.ONLINE) {
2391			final boolean ask = contact.getOption(Contact.Options.ASKING);
2392			final boolean sendUpdates = contact
2393					.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
2394					&& contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
2395			final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2396			iq.query(Xmlns.ROSTER).addChild(contact.asElement());
2397			account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
2398			if (sendUpdates) {
2399				sendPresencePacket(account,
2400						mPresenceGenerator.sendPresenceUpdatesTo(contact));
2401			}
2402			if (ask) {
2403				sendPresencePacket(account,
2404						mPresenceGenerator.requestPresenceUpdatesFrom(contact));
2405			}
2406		}
2407	}
2408
2409	public void publishAvatar(Account account, Uri image, UiCallback<Avatar> callback) {
2410		final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
2411		final int size = Config.AVATAR_SIZE;
2412		final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
2413		if (avatar != null) {
2414			avatar.height = size;
2415			avatar.width = size;
2416			if (format.equals(Bitmap.CompressFormat.WEBP)) {
2417				avatar.type = "image/webp";
2418			} else if (format.equals(Bitmap.CompressFormat.JPEG)) {
2419				avatar.type = "image/jpeg";
2420			} else if (format.equals(Bitmap.CompressFormat.PNG)) {
2421				avatar.type = "image/png";
2422			}
2423			if (!getFileBackend().save(avatar)) {
2424				callback.error(R.string.error_saving_avatar, avatar);
2425				return;
2426			}
2427			publishAvatar(account, avatar, callback);
2428		} else {
2429			callback.error(R.string.error_publish_avatar_converting, null);
2430		}
2431	}
2432
2433	public void publishAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2434		final IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
2435		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2436
2437			@Override
2438			public void onIqPacketReceived(Account account, IqPacket result) {
2439				if (result.getType() == IqPacket.TYPE.RESULT) {
2440					final IqPacket packet = XmppConnectionService.this.mIqGenerator
2441							.publishAvatarMetadata(avatar);
2442					sendIqPacket(account, packet, new OnIqPacketReceived() {
2443						@Override
2444						public void onIqPacketReceived(Account account, IqPacket result) {
2445							if (result.getType() == IqPacket.TYPE.RESULT) {
2446								if (account.setAvatar(avatar.getFilename())) {
2447									getAvatarService().clear(account);
2448									databaseBackend.updateAccount(account);
2449								}
2450								if (callback != null) {
2451									callback.success(avatar);
2452								} else {
2453									Log.d(Config.LOGTAG,account.getJid().toBareJid()+": published avatar");
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				} else {
2465					if (callback != null) {
2466						callback.error(
2467								R.string.error_publish_avatar_server_reject,
2468								avatar);
2469					}
2470				}
2471			}
2472		});
2473	}
2474
2475	public void republishAvatarIfNeeded(Account account) {
2476		if (account.getAxolotlService().isPepBroken()) {
2477			Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping republication of avatar because pep is broken");
2478			return;
2479		}
2480		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
2481		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2482
2483			private Avatar parseAvatar(IqPacket packet) {
2484				Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
2485				if (pubsub != null) {
2486					Element items = pubsub.findChild("items");
2487					if (items != null) {
2488						return Avatar.parseMetadata(items);
2489					}
2490				}
2491				return null;
2492			}
2493
2494			private boolean errorIsItemNotFound(IqPacket packet) {
2495				Element error = packet.findChild("error");
2496				return packet.getType() == IqPacket.TYPE.ERROR
2497						&& error != null
2498						&& error.hasChild("item-not-found");
2499			}
2500
2501			@Override
2502			public void onIqPacketReceived(Account account, IqPacket packet) {
2503				if (packet.getType() == IqPacket.TYPE.RESULT || errorIsItemNotFound(packet)) {
2504					Avatar serverAvatar = parseAvatar(packet);
2505					if (serverAvatar == null && account.getAvatar() != null) {
2506						Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar());
2507						if (avatar != null) {
2508							Log.d(Config.LOGTAG,account.getJid().toBareJid()+": avatar on server was null. republishing");
2509							publishAvatar(account, fileBackend.getStoredPepAvatar(account.getAvatar()), null);
2510						} else {
2511							Log.e(Config.LOGTAG, account.getJid().toBareJid()+": error rereading avatar");
2512						}
2513					}
2514				}
2515			}
2516		});
2517	}
2518
2519	public void fetchAvatar(Account account, Avatar avatar) {
2520		fetchAvatar(account, avatar, null);
2521	}
2522
2523	public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2524		final String KEY = generateFetchKey(account, avatar);
2525		synchronized (this.mInProgressAvatarFetches) {
2526			if (!this.mInProgressAvatarFetches.contains(KEY)) {
2527				switch (avatar.origin) {
2528					case PEP:
2529						this.mInProgressAvatarFetches.add(KEY);
2530						fetchAvatarPep(account, avatar, callback);
2531						break;
2532					case VCARD:
2533						this.mInProgressAvatarFetches.add(KEY);
2534						fetchAvatarVcard(account, avatar, callback);
2535						break;
2536				}
2537			}
2538		}
2539	}
2540
2541	private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2542		IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar);
2543		sendIqPacket(account, packet, new OnIqPacketReceived() {
2544
2545			@Override
2546			public void onIqPacketReceived(Account account, IqPacket result) {
2547				synchronized (mInProgressAvatarFetches) {
2548					mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
2549				}
2550				final String ERROR = account.getJid().toBareJid()
2551						+ ": fetching avatar for " + avatar.owner + " failed ";
2552				if (result.getType() == IqPacket.TYPE.RESULT) {
2553					avatar.image = mIqParser.avatarData(result);
2554					if (avatar.image != null) {
2555						if (getFileBackend().save(avatar)) {
2556							if (account.getJid().toBareJid().equals(avatar.owner)) {
2557								if (account.setAvatar(avatar.getFilename())) {
2558									databaseBackend.updateAccount(account);
2559								}
2560								getAvatarService().clear(account);
2561								updateConversationUi();
2562								updateAccountUi();
2563							} else {
2564								Contact contact = account.getRoster()
2565										.getContact(avatar.owner);
2566								contact.setAvatar(avatar);
2567								getAvatarService().clear(contact);
2568								updateConversationUi();
2569								updateRosterUi();
2570							}
2571							if (callback != null) {
2572								callback.success(avatar);
2573							}
2574							Log.d(Config.LOGTAG, account.getJid().toBareJid()
2575									+ ": successfully fetched pep avatar for " + avatar.owner);
2576							return;
2577						}
2578					} else {
2579
2580						Log.d(Config.LOGTAG, ERROR + "(parsing error)");
2581					}
2582				} else {
2583					Element error = result.findChild("error");
2584					if (error == null) {
2585						Log.d(Config.LOGTAG, ERROR + "(server error)");
2586					} else {
2587						Log.d(Config.LOGTAG, ERROR + error.toString());
2588					}
2589				}
2590				if (callback != null) {
2591					callback.error(0, null);
2592				}
2593
2594			}
2595		});
2596	}
2597
2598	private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2599		IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
2600		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2601			@Override
2602			public void onIqPacketReceived(Account account, IqPacket packet) {
2603				synchronized (mInProgressAvatarFetches) {
2604					mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
2605				}
2606				if (packet.getType() == IqPacket.TYPE.RESULT) {
2607					Element vCard = packet.findChild("vCard", "vcard-temp");
2608					Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
2609					String image = photo != null ? photo.findChildContent("BINVAL") : null;
2610					if (image != null) {
2611						avatar.image = image;
2612						if (getFileBackend().save(avatar)) {
2613							Log.d(Config.LOGTAG, account.getJid().toBareJid()
2614									+ ": successfully fetched vCard avatar for " + avatar.owner);
2615							if (avatar.owner.isBareJid()) {
2616								Contact contact = account.getRoster()
2617										.getContact(avatar.owner);
2618								contact.setAvatar(avatar);
2619								getAvatarService().clear(contact);
2620								updateConversationUi();
2621								updateRosterUi();
2622							} else {
2623								Conversation conversation = find(account, avatar.owner.toBareJid());
2624								if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
2625									MucOptions.User user = conversation.getMucOptions().findUserByFullJid(avatar.owner);
2626									if (user != null) {
2627										if (user.setAvatar(avatar)) {
2628											getAvatarService().clear(user);
2629											updateConversationUi();
2630											updateMucRosterUi();
2631										}
2632									}
2633								}
2634							}
2635						}
2636					}
2637				}
2638			}
2639		});
2640	}
2641
2642	public void checkForAvatar(Account account, final UiCallback<Avatar> callback) {
2643		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
2644		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2645
2646			@Override
2647			public void onIqPacketReceived(Account account, IqPacket packet) {
2648				if (packet.getType() == IqPacket.TYPE.RESULT) {
2649					Element pubsub = packet.findChild("pubsub","http://jabber.org/protocol/pubsub");
2650					if (pubsub != null) {
2651						Element items = pubsub.findChild("items");
2652						if (items != null) {
2653							Avatar avatar = Avatar.parseMetadata(items);
2654							if (avatar != null) {
2655								avatar.owner = account.getJid().toBareJid();
2656								if (fileBackend.isAvatarCached(avatar)) {
2657									if (account.setAvatar(avatar.getFilename())) {
2658										databaseBackend.updateAccount(account);
2659									}
2660									getAvatarService().clear(account);
2661									callback.success(avatar);
2662								} else {
2663									fetchAvatarPep(account, avatar, callback);
2664								}
2665								return;
2666							}
2667						}
2668					}
2669				}
2670				callback.error(0, null);
2671			}
2672		});
2673	}
2674
2675	public void deleteContactOnServer(Contact contact) {
2676		contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
2677		contact.resetOption(Contact.Options.DIRTY_PUSH);
2678		contact.setOption(Contact.Options.DIRTY_DELETE);
2679		Account account = contact.getAccount();
2680		if (account.getStatus() == Account.State.ONLINE) {
2681			IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2682			Element item = iq.query(Xmlns.ROSTER).addChild("item");
2683			item.setAttribute("jid", contact.getJid().toString());
2684			item.setAttribute("subscription", "remove");
2685			account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
2686		}
2687	}
2688
2689	public void updateConversation(Conversation conversation) {
2690		this.databaseBackend.updateConversation(conversation);
2691	}
2692
2693	private void reconnectAccount(final Account account, final boolean force, final boolean interactive) {
2694		synchronized (account) {
2695			XmppConnection connection = account.getXmppConnection();
2696			if (connection == null) {
2697				connection = createConnection(account);
2698				account.setXmppConnection(connection);
2699			} else {
2700				connection.interrupt();
2701			}
2702			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
2703				if (!force) {
2704					disconnect(account, false);
2705				}
2706				Thread thread = new Thread(connection);
2707				connection.setInteractive(interactive);
2708				connection.prepareNewConnection();
2709				thread.start();
2710				scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
2711			} else {
2712				disconnect(account, force);
2713				account.getRoster().clearPresences();
2714				connection.resetEverything();
2715				account.getAxolotlService().resetBrokenness();
2716			}
2717		}
2718	}
2719
2720	public void reconnectAccountInBackground(final Account account) {
2721		new Thread(new Runnable() {
2722			@Override
2723			public void run() {
2724				reconnectAccount(account, false, true);
2725			}
2726		}).start();
2727	}
2728
2729	public void invite(Conversation conversation, Jid contact) {
2730		Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": inviting " + contact + " to " + conversation.getJid().toBareJid());
2731		MessagePacket packet = mMessageGenerator.invite(conversation, contact);
2732		sendMessagePacket(conversation.getAccount(), packet);
2733	}
2734
2735	public void directInvite(Conversation conversation, Jid jid) {
2736		MessagePacket packet = mMessageGenerator.directInvite(conversation, jid);
2737		sendMessagePacket(conversation.getAccount(), packet);
2738	}
2739
2740	public void resetSendingToWaiting(Account account) {
2741		for (Conversation conversation : getConversations()) {
2742			if (conversation.getAccount() == account) {
2743				conversation.findUnsentTextMessages(new Conversation.OnMessageFound() {
2744
2745					@Override
2746					public void onMessageFound(Message message) {
2747						markMessage(message, Message.STATUS_WAITING);
2748					}
2749				});
2750			}
2751		}
2752	}
2753
2754	public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) {
2755		if (uuid == null) {
2756			return null;
2757		}
2758		for (Conversation conversation : getConversations()) {
2759			if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) {
2760				final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
2761				if (message != null) {
2762					markMessage(message, status);
2763				}
2764				return message;
2765			}
2766		}
2767		return null;
2768	}
2769
2770	public boolean markMessage(Conversation conversation, String uuid, int status) {
2771		if (uuid == null) {
2772			return false;
2773		} else {
2774			Message message = conversation.findSentMessageWithUuid(uuid);
2775			if (message != null) {
2776				markMessage(message, status);
2777				return true;
2778			} else {
2779				return false;
2780			}
2781		}
2782	}
2783
2784	public void markMessage(Message message, int status) {
2785		if (status == Message.STATUS_SEND_FAILED
2786				&& (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
2787				.getStatus() == Message.STATUS_SEND_DISPLAYED)) {
2788			return;
2789		}
2790		message.setStatus(status);
2791		databaseBackend.updateMessage(message);
2792		updateConversationUi();
2793	}
2794
2795	public SharedPreferences getPreferences() {
2796		return PreferenceManager
2797				.getDefaultSharedPreferences(getApplicationContext());
2798	}
2799
2800	public boolean confirmMessages() {
2801		return getPreferences().getBoolean("confirm_messages", true);
2802	}
2803
2804	public boolean allowMessageCorrection() {
2805		return getPreferences().getBoolean("allow_message_correction", false);
2806	}
2807
2808	public boolean sendChatStates() {
2809		return getPreferences().getBoolean("chat_states", false);
2810	}
2811
2812	public boolean saveEncryptedMessages() {
2813		return !getPreferences().getBoolean("dont_save_encrypted", false);
2814	}
2815
2816	private boolean respectAutojoin() {
2817		return getPreferences().getBoolean("autojoin", true);
2818	}
2819
2820	public boolean indicateReceived() {
2821		return getPreferences().getBoolean("indicate_received", false);
2822	}
2823
2824	public boolean useTorToConnect() {
2825		return Config.FORCE_ORBOT || getPreferences().getBoolean("use_tor", false);
2826	}
2827
2828	public boolean showExtendedConnectionOptions() {
2829		return getPreferences().getBoolean("show_connection_options", false);
2830	}
2831
2832	public boolean broadcastLastActivity() {
2833		return getPreferences().getBoolean("last_activity", false);
2834	}
2835
2836	public int unreadCount() {
2837		int count = 0;
2838		for (Conversation conversation : getConversations()) {
2839			count += conversation.unreadCount();
2840		}
2841		return count;
2842	}
2843
2844
2845	public void showErrorToastInUi(int resId) {
2846		if (mOnShowErrorToast != null) {
2847			mOnShowErrorToast.onShowErrorToast(resId);
2848		}
2849	}
2850
2851	public void updateConversationUi() {
2852		if (mOnConversationUpdate != null) {
2853			mOnConversationUpdate.onConversationUpdate();
2854		}
2855	}
2856
2857	public void updateAccountUi() {
2858		if (mOnAccountUpdate != null) {
2859			mOnAccountUpdate.onAccountUpdate();
2860		}
2861	}
2862
2863	public void updateRosterUi() {
2864		if (mOnRosterUpdate != null) {
2865			mOnRosterUpdate.onRosterUpdate();
2866		}
2867	}
2868
2869	public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
2870		if (mOnCaptchaRequested != null) {
2871			DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
2872			Bitmap scaled = Bitmap.createScaledBitmap(captcha, (int) (captcha.getWidth() * metrics.scaledDensity),
2873					(int) (captcha.getHeight() * metrics.scaledDensity), false);
2874
2875			mOnCaptchaRequested.onCaptchaRequested(account, id, data, scaled);
2876			return true;
2877		}
2878		return false;
2879	}
2880
2881	public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
2882		if (mOnUpdateBlocklist != null) {
2883			mOnUpdateBlocklist.OnUpdateBlocklist(status);
2884		}
2885	}
2886
2887	public void updateMucRosterUi() {
2888		if (mOnMucRosterUpdate != null) {
2889			mOnMucRosterUpdate.onMucRosterUpdate();
2890		}
2891	}
2892
2893	public void keyStatusUpdated(AxolotlService.FetchStatus report) {
2894		if (mOnKeyStatusUpdated != null) {
2895			mOnKeyStatusUpdated.onKeyStatusUpdated(report);
2896		}
2897	}
2898
2899	public Account findAccountByJid(final Jid accountJid) {
2900		for (Account account : this.accounts) {
2901			if (account.getJid().toBareJid().equals(accountJid.toBareJid())) {
2902				return account;
2903			}
2904		}
2905		return null;
2906	}
2907
2908	public Conversation findConversationByUuid(String uuid) {
2909		for (Conversation conversation : getConversations()) {
2910			if (conversation.getUuid().equals(uuid)) {
2911				return conversation;
2912			}
2913		}
2914		return null;
2915	}
2916
2917	public boolean markRead(final Conversation conversation) {
2918		mNotificationService.clear(conversation);
2919		final List<Message> readMessages = conversation.markRead();
2920		if (readMessages.size() > 0) {
2921			Runnable runnable = new Runnable() {
2922				@Override
2923				public void run() {
2924					for (Message message : readMessages) {
2925						databaseBackend.updateMessage(message);
2926					}
2927				}
2928			};
2929			mDatabaseExecutor.execute(runnable);
2930			updateUnreadCountBadge();
2931			return true;
2932		} else {
2933			return false;
2934		}
2935	}
2936
2937	public synchronized void updateUnreadCountBadge() {
2938		int count = unreadCount();
2939		if (unreadCount != count) {
2940			Log.d(Config.LOGTAG, "update unread count to " + count);
2941			if (count > 0) {
2942				ShortcutBadger.applyCount(getApplicationContext(), count);
2943			} else {
2944				ShortcutBadger.removeCount(getApplicationContext());
2945			}
2946			unreadCount = count;
2947		}
2948	}
2949
2950	public void sendReadMarker(final Conversation conversation) {
2951		final Message markable = conversation.getLatestMarkableMessage();
2952		if (this.markRead(conversation)) {
2953			updateConversationUi();
2954		}
2955		if (confirmMessages() && markable != null && markable.getRemoteMsgId() != null) {
2956			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
2957			Account account = conversation.getAccount();
2958			final Jid to = markable.getCounterpart();
2959			MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId());
2960			this.sendMessagePacket(conversation.getAccount(), packet);
2961		}
2962	}
2963
2964	public SecureRandom getRNG() {
2965		return this.mRandom;
2966	}
2967
2968	public MemorizingTrustManager getMemorizingTrustManager() {
2969		return this.mMemorizingTrustManager;
2970	}
2971
2972	public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
2973		this.mMemorizingTrustManager = trustManager;
2974	}
2975
2976	public void updateMemorizingTrustmanager() {
2977		final MemorizingTrustManager tm;
2978		final boolean dontTrustSystemCAs = getPreferences().getBoolean("dont_trust_system_cas", false);
2979		if (dontTrustSystemCAs) {
2980			tm = new MemorizingTrustManager(getApplicationContext(), null);
2981		} else {
2982			tm = new MemorizingTrustManager(getApplicationContext());
2983		}
2984		setMemorizingTrustManager(tm);
2985	}
2986
2987	public PowerManager getPowerManager() {
2988		return this.pm;
2989	}
2990
2991	public LruCache<String, Bitmap> getBitmapCache() {
2992		return this.mBitmapCache;
2993	}
2994
2995	public void syncRosterToDisk(final Account account) {
2996		Runnable runnable = new Runnable() {
2997
2998			@Override
2999			public void run() {
3000				databaseBackend.writeRoster(account.getRoster());
3001			}
3002		};
3003		mDatabaseExecutor.execute(runnable);
3004
3005	}
3006
3007	public List<String> getKnownHosts() {
3008		final List<String> hosts = new ArrayList<>();
3009		for (final Account account : getAccounts()) {
3010			if (!hosts.contains(account.getServer().toString())) {
3011				hosts.add(account.getServer().toString());
3012			}
3013			for (final Contact contact : account.getRoster().getContacts()) {
3014				if (contact.showInRoster()) {
3015					final String server = contact.getServer().toString();
3016					if (server != null && !hosts.contains(server)) {
3017						hosts.add(server);
3018					}
3019				}
3020			}
3021		}
3022		if(Config.DOMAIN_LOCK != null && !hosts.contains(Config.DOMAIN_LOCK)) {
3023			hosts.add(Config.DOMAIN_LOCK);
3024		}
3025		if(Config.MAGIC_CREATE_DOMAIN != null && !hosts.contains(Config.MAGIC_CREATE_DOMAIN)) {
3026			hosts.add(Config.MAGIC_CREATE_DOMAIN);
3027		}
3028		return hosts;
3029	}
3030
3031	public List<String> getKnownConferenceHosts() {
3032		final ArrayList<String> mucServers = new ArrayList<>();
3033		for (final Account account : accounts) {
3034			if (account.getXmppConnection() != null) {
3035				final String server = account.getXmppConnection().getMucServer();
3036				if (server != null && !mucServers.contains(server)) {
3037					mucServers.add(server);
3038				}
3039			}
3040		}
3041		return mucServers;
3042	}
3043
3044	public void sendMessagePacket(Account account, MessagePacket packet) {
3045		XmppConnection connection = account.getXmppConnection();
3046		if (connection != null) {
3047			connection.sendMessagePacket(packet);
3048		}
3049	}
3050
3051	public void sendPresencePacket(Account account, PresencePacket packet) {
3052		XmppConnection connection = account.getXmppConnection();
3053		if (connection != null) {
3054			connection.sendPresencePacket(packet);
3055		}
3056	}
3057
3058	public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
3059		final XmppConnection connection = account.getXmppConnection();
3060		if (connection != null) {
3061			IqPacket request = mIqGenerator.generateCreateAccountWithCaptcha(account, id, data);
3062			sendIqPacket(account, request, connection.registrationResponseListener);
3063		}
3064	}
3065
3066	public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
3067		final XmppConnection connection = account.getXmppConnection();
3068		if (connection != null) {
3069			connection.sendIqPacket(packet, callback);
3070		}
3071	}
3072
3073	public void sendPresence(final Account account) {
3074		sendPresence(account, checkListeners() && broadcastLastActivity());
3075	}
3076
3077	private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
3078		PresencePacket packet;
3079		if (manuallyChangePresence()) {
3080			packet =  mPresenceGenerator.selfPresence(account, account.getPresenceStatus());
3081			String message = account.getPresenceStatusMessage();
3082			if (message != null && !message.isEmpty()) {
3083				packet.addChild(new Element("status").setContent(message));
3084			}
3085		} else {
3086			packet = mPresenceGenerator.selfPresence(account, getTargetPresence());
3087		}
3088		if (mLastActivity > 0 && includeIdleTimestamp) {
3089			long since = Math.min(mLastActivity, System.currentTimeMillis()); //don't send future dates
3090			packet.addChild("idle","urn:xmpp:idle:1").setAttribute("since", AbstractGenerator.getTimestamp(since));
3091		}
3092		sendPresencePacket(account, packet);
3093	}
3094
3095	private void deactivateGracePeriod() {
3096		for(Account account : getAccounts()) {
3097			account.deactivateGracePeriod();
3098		}
3099	}
3100
3101	public void refreshAllPresences() {
3102		boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
3103		for (Account account : getAccounts()) {
3104			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
3105				sendPresence(account, includeIdleTimestamp);
3106			}
3107		}
3108	}
3109
3110	private void refreshAllGcmTokens() {
3111		for(Account account : getAccounts()) {
3112			if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
3113				mPushManagementService.registerPushTokenOnServer(account);
3114			}
3115		}
3116	}
3117
3118	public void sendOfflinePresence(final Account account) {
3119		sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
3120	}
3121
3122	public MessageGenerator getMessageGenerator() {
3123		return this.mMessageGenerator;
3124	}
3125
3126	public PresenceGenerator getPresenceGenerator() {
3127		return this.mPresenceGenerator;
3128	}
3129
3130	public IqGenerator getIqGenerator() {
3131		return this.mIqGenerator;
3132	}
3133
3134	public IqParser getIqParser() {
3135		return this.mIqParser;
3136	}
3137
3138	public JingleConnectionManager getJingleConnectionManager() {
3139		return this.mJingleConnectionManager;
3140	}
3141
3142	public MessageArchiveService getMessageArchiveService() {
3143		return this.mMessageArchiveService;
3144	}
3145
3146	public List<Contact> findContacts(Jid jid) {
3147		ArrayList<Contact> contacts = new ArrayList<>();
3148		for (Account account : getAccounts()) {
3149			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
3150				Contact contact = account.getRoster().getContactFromRoster(jid);
3151				if (contact != null) {
3152					contacts.add(contact);
3153				}
3154			}
3155		}
3156		return contacts;
3157	}
3158
3159	public Conversation findFirstMuc(Jid jid) {
3160		for(Conversation conversation : getConversations()) {
3161			if (conversation.getJid().toBareJid().equals(jid.toBareJid())
3162					&& conversation.getMode() == Conversation.MODE_MULTI) {
3163				return conversation;
3164			}
3165		}
3166		return null;
3167	}
3168
3169	public NotificationService getNotificationService() {
3170		return this.mNotificationService;
3171	}
3172
3173	public HttpConnectionManager getHttpConnectionManager() {
3174		return this.mHttpConnectionManager;
3175	}
3176
3177	public void resendFailedMessages(final Message message) {
3178		final Collection<Message> messages = new ArrayList<>();
3179		Message current = message;
3180		while (current.getStatus() == Message.STATUS_SEND_FAILED) {
3181			messages.add(current);
3182			if (current.mergeable(current.next())) {
3183				current = current.next();
3184			} else {
3185				break;
3186			}
3187		}
3188		for (final Message msg : messages) {
3189			msg.setTime(System.currentTimeMillis());
3190			markMessage(msg, Message.STATUS_WAITING);
3191			this.resendMessage(msg, false);
3192		}
3193	}
3194
3195	public void clearConversationHistory(final Conversation conversation) {
3196		conversation.clearMessages();
3197		conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam
3198		conversation.setLastClearHistory(System.currentTimeMillis());
3199		Runnable runnable = new Runnable() {
3200			@Override
3201			public void run() {
3202				databaseBackend.deleteMessagesInConversation(conversation);
3203			}
3204		};
3205		mDatabaseExecutor.execute(runnable);
3206	}
3207
3208	public void sendBlockRequest(final Blockable blockable) {
3209		if (blockable != null && blockable.getBlockedJid() != null) {
3210			final Jid jid = blockable.getBlockedJid();
3211			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid), new OnIqPacketReceived() {
3212
3213				@Override
3214				public void onIqPacketReceived(final Account account, final IqPacket packet) {
3215					if (packet.getType() == IqPacket.TYPE.RESULT) {
3216						account.getBlocklist().add(jid);
3217						updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
3218					}
3219				}
3220			});
3221		}
3222	}
3223
3224	public void sendUnblockRequest(final Blockable blockable) {
3225		if (blockable != null && blockable.getJid() != null) {
3226			final Jid jid = blockable.getBlockedJid();
3227			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() {
3228				@Override
3229				public void onIqPacketReceived(final Account account, final IqPacket packet) {
3230					if (packet.getType() == IqPacket.TYPE.RESULT) {
3231						account.getBlocklist().remove(jid);
3232						updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
3233					}
3234				}
3235			});
3236		}
3237	}
3238
3239	public void publishDisplayName(Account account) {
3240		String displayName = account.getDisplayName();
3241		if (displayName != null && !displayName.isEmpty()) {
3242			IqPacket publish = mIqGenerator.publishNick(displayName);
3243			sendIqPacket(account, publish, new OnIqPacketReceived() {
3244				@Override
3245				public void onIqPacketReceived(Account account, IqPacket packet) {
3246					if (packet.getType() == IqPacket.TYPE.ERROR) {
3247						Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not publish nick");
3248					}
3249				}
3250			});
3251		}
3252	}
3253
3254	public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String, String> key) {
3255		ServiceDiscoveryResult result = discoCache.get(key);
3256		if (result != null) {
3257			return result;
3258		} else {
3259			result = databaseBackend.findDiscoveryResult(key.first, key.second);
3260			if (result != null) {
3261				discoCache.put(key, result);
3262			}
3263			return result;
3264		}
3265	}
3266
3267	public void fetchCaps(Account account, final Jid jid, final Presence presence) {
3268		final Pair<String,String> key = new Pair<>(presence.getHash(), presence.getVer());
3269		ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key);
3270		if (disco != null) {
3271			presence.setServiceDiscoveryResult(disco);
3272		} else {
3273			if (!account.inProgressDiscoFetches.contains(key)) {
3274				account.inProgressDiscoFetches.add(key);
3275				IqPacket request = new IqPacket(IqPacket.TYPE.GET);
3276				request.setTo(jid);
3277				request.query("http://jabber.org/protocol/disco#info");
3278				Log.d(Config.LOGTAG,account.getJid().toBareJid()+": making disco request for "+key.second+" to "+jid);
3279				sendIqPacket(account, request, new OnIqPacketReceived() {
3280					@Override
3281					public void onIqPacketReceived(Account account, IqPacket discoPacket) {
3282						if (discoPacket.getType() == IqPacket.TYPE.RESULT) {
3283							ServiceDiscoveryResult disco = new ServiceDiscoveryResult(discoPacket);
3284							if (presence.getVer().equals(disco.getVer())) {
3285								databaseBackend.insertDiscoveryResult(disco);
3286								injectServiceDiscorveryResult(account.getRoster(), presence.getHash(), presence.getVer(), disco);
3287							} else {
3288								Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + disco.getVer());
3289							}
3290						}
3291						account.inProgressDiscoFetches.remove(key);
3292					}
3293				});
3294			}
3295		}
3296	}
3297
3298	private void injectServiceDiscorveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) {
3299		for(Contact contact : roster.getContacts()) {
3300			for(Presence presence : contact.getPresences().getPresences().values()) {
3301				if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
3302					presence.setServiceDiscoveryResult(disco);
3303				}
3304			}
3305		}
3306	}
3307
3308	public void fetchMamPreferences(Account account, final OnMamPreferencesFetched callback) {
3309		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
3310		request.addChild("prefs","urn:xmpp:mam:0");
3311		sendIqPacket(account, request, new OnIqPacketReceived() {
3312			@Override
3313			public void onIqPacketReceived(Account account, IqPacket packet) {
3314				Element prefs = packet.findChild("prefs","urn:xmpp:mam:0");
3315				if (packet.getType() == IqPacket.TYPE.RESULT && prefs != null) {
3316					callback.onPreferencesFetched(prefs);
3317				} else {
3318					callback.onPreferencesFetchFailed();
3319				}
3320			}
3321		});
3322	}
3323
3324	public PushManagementService getPushManagementService() {
3325		return mPushManagementService;
3326	}
3327
3328	public Account getPendingAccount() {
3329		Account pending = null;
3330		for(Account account : getAccounts()) {
3331			if (account.isOptionSet(Account.OPTION_REGISTER)) {
3332				pending = account;
3333			} else {
3334				return null;
3335			}
3336		}
3337		return pending;
3338	}
3339
3340	public void changeStatus(Account account, Presence.Status status, String statusMessage, boolean send) {
3341		if (!statusMessage.isEmpty()) {
3342			databaseBackend.insertPresenceTemplate(new PresenceTemplate(status, statusMessage));
3343		}
3344		changeStatusReal(account, status, statusMessage, send);
3345	}
3346
3347	private void changeStatusReal(Account account, Presence.Status status, String statusMessage, boolean send) {
3348		account.setPresenceStatus(status);
3349		account.setPresenceStatusMessage(statusMessage);
3350		databaseBackend.updateAccount(account);
3351		if (!account.isOptionSet(Account.OPTION_DISABLED) && send) {
3352			sendPresence(account);
3353		}
3354	}
3355
3356	public void changeStatus(Presence.Status status, String statusMessage) {
3357		if (!statusMessage.isEmpty()) {
3358			databaseBackend.insertPresenceTemplate(new PresenceTemplate(status, statusMessage));
3359		}
3360		for(Account account : getAccounts()) {
3361			changeStatusReal(account, status, statusMessage, true);
3362		}
3363	}
3364
3365	public List<PresenceTemplate> getPresenceTemplates(Account account) {
3366		List<PresenceTemplate> templates = databaseBackend.getPresenceTemplates();
3367		for(PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) {
3368			if (!templates.contains(template)) {
3369				templates.add(0, template);
3370			}
3371		}
3372		return templates;
3373	}
3374
3375	public void saveConversationAsBookmark(Conversation conversation, String name) {
3376		Account account = conversation.getAccount();
3377		Bookmark bookmark = new Bookmark(account, conversation.getJid().toBareJid());
3378		if (!conversation.getJid().isBareJid()) {
3379			bookmark.setNick(conversation.getJid().getResourcepart());
3380		}
3381		if (name != null && !name.trim().isEmpty()) {
3382			bookmark.setBookmarkName(name.trim());
3383		}
3384		bookmark.setAutojoin(getPreferences().getBoolean("autojoin",true));
3385		account.getBookmarks().add(bookmark);
3386		pushBookmarks(account);
3387		conversation.setBookmark(bookmark);
3388	}
3389
3390	public interface OnMamPreferencesFetched {
3391		void onPreferencesFetched(Element prefs);
3392		void onPreferencesFetchFailed();
3393	}
3394
3395	public void pushMamPreferences(Account account, Element prefs) {
3396		IqPacket set = new IqPacket(IqPacket.TYPE.SET);
3397		set.addChild(prefs);
3398		sendIqPacket(account, set, null);
3399	}
3400
3401	public interface OnAccountCreated {
3402		void onAccountCreated(Account account);
3403
3404		void informUser(int r);
3405	}
3406
3407	public interface OnMoreMessagesLoaded {
3408		void onMoreMessagesLoaded(int count, Conversation conversation);
3409
3410		void informUser(int r);
3411	}
3412
3413	public interface OnAccountPasswordChanged {
3414		void onPasswordChangeSucceeded();
3415
3416		void onPasswordChangeFailed();
3417	}
3418
3419	public interface OnAffiliationChanged {
3420		void onAffiliationChangedSuccessful(Jid jid);
3421
3422		void onAffiliationChangeFailed(Jid jid, int resId);
3423	}
3424
3425	public interface OnRoleChanged {
3426		void onRoleChangedSuccessful(String nick);
3427
3428		void onRoleChangeFailed(String nick, int resid);
3429	}
3430
3431	public interface OnConversationUpdate {
3432		void onConversationUpdate();
3433	}
3434
3435	public interface OnAccountUpdate {
3436		void onAccountUpdate();
3437	}
3438
3439	public interface OnCaptchaRequested {
3440		void onCaptchaRequested(Account account,
3441								String id,
3442								Data data,
3443								Bitmap captcha);
3444	}
3445
3446	public interface OnRosterUpdate {
3447		void onRosterUpdate();
3448	}
3449
3450	public interface OnMucRosterUpdate {
3451		void onMucRosterUpdate();
3452	}
3453
3454	public interface OnConferenceConfigurationFetched {
3455		void onConferenceConfigurationFetched(Conversation conversation);
3456
3457		void onFetchFailed(Conversation conversation, Element error);
3458	}
3459
3460	public interface OnConferenceJoined {
3461		void onConferenceJoined(Conversation conversation);
3462	}
3463
3464	public interface OnConferenceOptionsPushed {
3465		void onPushSucceeded();
3466
3467		void onPushFailed();
3468	}
3469
3470	public interface OnShowErrorToast {
3471		void onShowErrorToast(int resId);
3472	}
3473
3474	public class XmppConnectionBinder extends Binder {
3475		public XmppConnectionService getService() {
3476			return XmppConnectionService.this;
3477		}
3478	}
3479}