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