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