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