XmppConnectionService.java

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