XmppConnectionService.java

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