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