XmppConnectionService.java

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