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