XmppConnectionService.java

   1package eu.siacs.conversations.services;
   2
   3import android.Manifest;
   4import android.annotation.SuppressLint;
   5import android.annotation.TargetApi;
   6import android.app.AlarmManager;
   7import android.app.NotificationManager;
   8import android.app.PendingIntent;
   9import android.app.Service;
  10import android.content.BroadcastReceiver;
  11import android.content.Context;
  12import android.content.Intent;
  13import android.content.IntentFilter;
  14import android.content.SharedPreferences;
  15import android.content.pm.PackageManager;
  16import android.database.ContentObserver;
  17import android.graphics.Bitmap;
  18import android.media.AudioManager;
  19import android.net.ConnectivityManager;
  20import android.net.NetworkInfo;
  21import android.net.Uri;
  22import android.os.Binder;
  23import android.os.Build;
  24import android.os.Bundle;
  25import android.os.Environment;
  26import android.os.IBinder;
  27import android.os.PowerManager;
  28import android.os.PowerManager.WakeLock;
  29import android.os.SystemClock;
  30import android.preference.PreferenceManager;
  31import android.provider.ContactsContract;
  32import android.security.KeyChain;
  33import android.support.annotation.BoolRes;
  34import android.support.annotation.IntegerRes;
  35import android.support.v4.app.RemoteInput;
  36import android.support.v4.content.ContextCompat;
  37import android.text.TextUtils;
  38import android.util.DisplayMetrics;
  39import android.util.Log;
  40import android.util.LruCache;
  41import android.util.Pair;
  42
  43import org.conscrypt.Conscrypt;
  44import org.openintents.openpgp.IOpenPgpService2;
  45import org.openintents.openpgp.util.OpenPgpApi;
  46import org.openintents.openpgp.util.OpenPgpServiceConnection;
  47
  48import java.io.File;
  49import java.net.URL;
  50import java.security.SecureRandom;
  51import java.security.Security;
  52import java.security.cert.CertificateException;
  53import java.security.cert.X509Certificate;
  54import java.util.ArrayList;
  55import java.util.Arrays;
  56import java.util.Collection;
  57import java.util.Collections;
  58import java.util.HashMap;
  59import java.util.HashSet;
  60import java.util.Hashtable;
  61import java.util.Iterator;
  62import java.util.List;
  63import java.util.ListIterator;
  64import java.util.Map;
  65import java.util.Set;
  66import java.util.WeakHashMap;
  67import java.util.concurrent.CopyOnWriteArrayList;
  68import java.util.concurrent.CountDownLatch;
  69import java.util.concurrent.atomic.AtomicBoolean;
  70import java.util.concurrent.atomic.AtomicLong;
  71
  72
  73import eu.siacs.conversations.Config;
  74import eu.siacs.conversations.R;
  75import eu.siacs.conversations.android.JabberIdContact;
  76import eu.siacs.conversations.crypto.OmemoSetting;
  77import eu.siacs.conversations.crypto.PgpDecryptionService;
  78import eu.siacs.conversations.crypto.PgpEngine;
  79import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  80import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
  81import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
  82import eu.siacs.conversations.entities.Account;
  83import eu.siacs.conversations.entities.Blockable;
  84import eu.siacs.conversations.entities.Bookmark;
  85import eu.siacs.conversations.entities.Contact;
  86import eu.siacs.conversations.entities.Conversation;
  87import eu.siacs.conversations.entities.Conversational;
  88import eu.siacs.conversations.entities.Message;
  89import eu.siacs.conversations.entities.MucOptions;
  90import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
  91import eu.siacs.conversations.entities.Presence;
  92import eu.siacs.conversations.entities.PresenceTemplate;
  93import eu.siacs.conversations.entities.Roster;
  94import eu.siacs.conversations.entities.ServiceDiscoveryResult;
  95import eu.siacs.conversations.generator.AbstractGenerator;
  96import eu.siacs.conversations.generator.IqGenerator;
  97import eu.siacs.conversations.generator.MessageGenerator;
  98import eu.siacs.conversations.generator.PresenceGenerator;
  99import eu.siacs.conversations.http.HttpConnectionManager;
 100import eu.siacs.conversations.http.CustomURLStreamHandlerFactory;
 101import eu.siacs.conversations.parser.AbstractParser;
 102import eu.siacs.conversations.parser.IqParser;
 103import eu.siacs.conversations.parser.MessageParser;
 104import eu.siacs.conversations.parser.PresenceParser;
 105import eu.siacs.conversations.persistance.DatabaseBackend;
 106import eu.siacs.conversations.persistance.FileBackend;
 107import eu.siacs.conversations.ui.SettingsActivity;
 108import eu.siacs.conversations.ui.UiCallback;
 109import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
 110import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
 111import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
 112import eu.siacs.conversations.utils.Compatibility;
 113import eu.siacs.conversations.utils.ConversationsFileObserver;
 114import eu.siacs.conversations.utils.CryptoHelper;
 115import eu.siacs.conversations.utils.ExceptionHelper;
 116import eu.siacs.conversations.utils.MimeUtils;
 117import eu.siacs.conversations.utils.PhoneHelper;
 118import eu.siacs.conversations.utils.QuickLoader;
 119import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
 120import eu.siacs.conversations.utils.ReplacingTaskManager;
 121import eu.siacs.conversations.utils.Resolver;
 122import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
 123import eu.siacs.conversations.utils.StringUtils;
 124import eu.siacs.conversations.utils.WakeLockHelper;
 125import eu.siacs.conversations.xml.Namespace;
 126import eu.siacs.conversations.utils.XmppUri;
 127import eu.siacs.conversations.xml.Element;
 128import eu.siacs.conversations.xmpp.OnBindListener;
 129import eu.siacs.conversations.xmpp.OnContactStatusChanged;
 130import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 131import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 132import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
 133import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
 134import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
 135import eu.siacs.conversations.xmpp.OnStatusChanged;
 136import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 137import eu.siacs.conversations.xmpp.Patches;
 138import eu.siacs.conversations.xmpp.XmppConnection;
 139import eu.siacs.conversations.xmpp.chatstate.ChatState;
 140import eu.siacs.conversations.xmpp.forms.Data;
 141import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
 142import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
 143import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
 144import eu.siacs.conversations.xmpp.mam.MamReference;
 145import eu.siacs.conversations.xmpp.pep.Avatar;
 146import eu.siacs.conversations.xmpp.pep.PublishOptions;
 147import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 148import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 149import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 150import me.leolin.shortcutbadger.ShortcutBadger;
 151import rocks.xmpp.addr.Jid;
 152
 153public class XmppConnectionService extends Service {
 154
 155    public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations";
 156    public static final String ACTION_MARK_AS_READ = "mark_as_read";
 157    public static final String ACTION_SNOOZE = "snooze";
 158    public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification";
 159    public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error";
 160    public static final String ACTION_TRY_AGAIN = "try_again";
 161    public static final String ACTION_IDLE_PING = "idle_ping";
 162    public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh";
 163    public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received";
 164
 165    private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
 166
 167    static {
 168        URL.setURLStreamHandlerFactory(new CustomURLStreamHandlerFactory());
 169    }
 170
 171    public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
 172    private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor("FileAdding");
 173    private final SerialSingleThreadExecutor mVideoCompressionExecutor = new SerialSingleThreadExecutor("VideoCompression");
 174    private final SerialSingleThreadExecutor mDatabaseWriterExecutor = new SerialSingleThreadExecutor("DatabaseWriter");
 175    private final SerialSingleThreadExecutor mDatabaseReaderExecutor = new SerialSingleThreadExecutor("DatabaseReader");
 176    private final SerialSingleThreadExecutor mNotificationExecutor = new SerialSingleThreadExecutor("NotificationExecutor");
 177    private final ReplacingTaskManager mRosterSyncTaskManager = new ReplacingTaskManager();
 178    private final IBinder mBinder = new XmppConnectionBinder();
 179    private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
 180    private final IqGenerator mIqGenerator = new IqGenerator(this);
 181    private final List<String> mInProgressAvatarFetches = new ArrayList<>();
 182    private final HashSet<Jid> mLowPingTimeoutMode = new HashSet<>();
 183    private final OnIqPacketReceived mDefaultIqHandler = (account, packet) -> {
 184        if (packet.getType() != IqPacket.TYPE.RESULT) {
 185            Element error = packet.findChild("error");
 186            String text = error != null ? error.findChildContent("text") : null;
 187            if (text != null) {
 188                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received iq error - " + text);
 189            }
 190        }
 191    };
 192    public DatabaseBackend databaseBackend;
 193    private ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger");
 194    private long mLastActivity = 0;
 195    private FileBackend fileBackend = new FileBackend(this);
 196    private MemorizingTrustManager mMemorizingTrustManager;
 197    private NotificationService mNotificationService = new NotificationService(this);
 198    private ShortcutService mShortcutService = new ShortcutService(this);
 199    private AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false);
 200    private AtomicBoolean mForceForegroundService = new AtomicBoolean(false);
 201    private OnMessagePacketReceived mMessageParser = new MessageParser(this);
 202    private OnPresencePacketReceived mPresenceParser = new PresenceParser(this);
 203    private IqParser mIqParser = new IqParser(this);
 204    private MessageGenerator mMessageGenerator = new MessageGenerator(this);
 205    public OnContactStatusChanged onContactStatusChanged = (contact, online) -> {
 206        Conversation conversation = find(getConversations(), contact);
 207        if (conversation != null) {
 208            if (online) {
 209                if (contact.getPresences().size() == 1) {
 210                    sendUnsentMessages(conversation);
 211                }
 212            }
 213        }
 214    };
 215    private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
 216    private List<Account> accounts;
 217    private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
 218            this);
 219    private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
 220
 221        @Override
 222        public void onJinglePacketReceived(Account account, JinglePacket packet) {
 223            mJingleConnectionManager.deliverPacket(account, packet);
 224        }
 225    };
 226    private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(this);
 227    private AvatarService mAvatarService = new AvatarService(this);
 228    private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
 229    private PushManagementService mPushManagementService = new PushManagementService(this);
 230    private QuickConversationsService mQuickConversationsService = new QuickConversationsService(this);
 231    private final ConversationsFileObserver fileObserver = new ConversationsFileObserver(
 232            Environment.getExternalStorageDirectory().getAbsolutePath()
 233    ) {
 234        @Override
 235        public void onEvent(int event, String path) {
 236            markFileDeleted(path);
 237        }
 238    };
 239    private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
 240
 241        @Override
 242        public boolean onMessageAcknowledged(Account account, String uuid) {
 243            for (final Conversation conversation : getConversations()) {
 244                if (conversation.getAccount() == account) {
 245                    Message message = conversation.findUnsentMessageWithUuid(uuid);
 246                    if (message != null) {
 247                        message.setStatus(Message.STATUS_SEND);
 248                        message.setErrorMessage(null);
 249                        databaseBackend.updateMessage(message, false);
 250                        return true;
 251                    }
 252                }
 253            }
 254            return false;
 255        }
 256    };
 257
 258    private int unreadCount = -1;
 259
 260    //Ui callback listeners
 261    private final Set<OnConversationUpdate> mOnConversationUpdates = Collections.newSetFromMap(new WeakHashMap<OnConversationUpdate, Boolean>());
 262    private final Set<OnShowErrorToast> mOnShowErrorToasts = Collections.newSetFromMap(new WeakHashMap<OnShowErrorToast, Boolean>());
 263    private final Set<OnAccountUpdate> mOnAccountUpdates = Collections.newSetFromMap(new WeakHashMap<OnAccountUpdate, Boolean>());
 264    private final Set<OnCaptchaRequested> mOnCaptchaRequested = Collections.newSetFromMap(new WeakHashMap<OnCaptchaRequested, Boolean>());
 265    private final Set<OnRosterUpdate> mOnRosterUpdates = Collections.newSetFromMap(new WeakHashMap<OnRosterUpdate, Boolean>());
 266    private final Set<OnUpdateBlocklist> mOnUpdateBlocklist = Collections.newSetFromMap(new WeakHashMap<OnUpdateBlocklist, Boolean>());
 267    private final Set<OnMucRosterUpdate> mOnMucRosterUpdate = Collections.newSetFromMap(new WeakHashMap<OnMucRosterUpdate, Boolean>());
 268    private final Set<OnKeyStatusUpdated> mOnKeyStatusUpdated = Collections.newSetFromMap(new WeakHashMap<OnKeyStatusUpdated, Boolean>());
 269
 270    private final Object LISTENER_LOCK = new Object();
 271
 272
 273    private final OnBindListener mOnBindListener = new OnBindListener() {
 274
 275        @Override
 276        public void onBind(final Account account) {
 277            synchronized (mInProgressAvatarFetches) {
 278                for (Iterator<String> iterator = mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) {
 279                    final String KEY = iterator.next();
 280                    if (KEY.startsWith(account.getJid().asBareJid() + "_")) {
 281                        iterator.remove();
 282                    }
 283                }
 284            }
 285            boolean loggedInSuccessfully = account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, true);
 286            boolean gainedFeature = account.setOption(Account.OPTION_HTTP_UPLOAD_AVAILABLE, account.getXmppConnection().getFeatures().httpUpload(0));
 287            if (loggedInSuccessfully || gainedFeature) {
 288                databaseBackend.updateAccount(account);
 289            }
 290
 291            if (loggedInSuccessfully) {
 292                if (!TextUtils.isEmpty(account.getDisplayName())) {
 293                    Log.d(Config.LOGTAG,account.getJid().asBareJid()+": display name wasn't empty on first log in. publishing");
 294                    publishDisplayName(account);
 295                }
 296            }
 297
 298            account.getRoster().clearPresences();
 299            mJingleConnectionManager.cancelInTransmission();
 300            mQuickConversationsService.considerSyncBackground(false);
 301            fetchRosterFromServer(account);
 302            if (!account.getXmppConnection().getFeatures().bookmarksConversion()) {
 303                fetchBookmarks(account);
 304            }
 305            final boolean flexible = account.getXmppConnection().getFeatures().flexibleOfflineMessageRetrieval();
 306            final boolean catchup = getMessageArchiveService().inCatchup(account);
 307            if (flexible && catchup && account.getXmppConnection().isMamPreferenceAlways()) {
 308                sendIqPacket(account, mIqGenerator.purgeOfflineMessages(), (acc, packet) -> {
 309                    if (packet.getType() == IqPacket.TYPE.RESULT) {
 310                        Log.d(Config.LOGTAG, acc.getJid().asBareJid() + ": successfully purged offline messages");
 311                    }
 312                });
 313            }
 314            sendPresence(account);
 315            if (mPushManagementService.available(account)) {
 316                mPushManagementService.registerPushTokenOnServer(account);
 317            }
 318            connectMultiModeConversations(account);
 319            syncDirtyContacts(account);
 320        }
 321    };
 322    private AtomicLong mLastExpiryRun = new AtomicLong(0);
 323    private SecureRandom mRandom;
 324    private LruCache<Pair<String, String>, ServiceDiscoveryResult> discoCache = new LruCache<>(20);
 325    private OnStatusChanged statusListener = new OnStatusChanged() {
 326
 327        @Override
 328        public void onStatusChanged(final Account account) {
 329            XmppConnection connection = account.getXmppConnection();
 330            updateAccountUi();
 331
 332            if (account.getStatus() == Account.State.ONLINE || account.getStatus().isError()) {
 333                mQuickConversationsService.signalAccountStateChange();
 334            }
 335
 336            if (account.getStatus() == Account.State.ONLINE) {
 337                synchronized (mLowPingTimeoutMode) {
 338                    if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
 339                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": leaving low ping timeout mode");
 340                    }
 341                }
 342                if (account.setShowErrorNotification(true)) {
 343                    databaseBackend.updateAccount(account);
 344                }
 345                mMessageArchiveService.executePendingQueries(account);
 346                if (connection != null && connection.getFeatures().csi()) {
 347                    if (checkListeners()) {
 348                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + " sending csi//inactive");
 349                        connection.sendInactive();
 350                    } else {
 351                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + " sending csi//active");
 352                        connection.sendActive();
 353                    }
 354                }
 355                List<Conversation> conversations = getConversations();
 356                for (Conversation conversation : conversations) {
 357                    if (conversation.getAccount() == account && !account.pendingConferenceJoins.contains(conversation)) {
 358                        sendUnsentMessages(conversation);
 359                    }
 360                }
 361                for (Conversation conversation : account.pendingConferenceLeaves) {
 362                    leaveMuc(conversation);
 363                }
 364                account.pendingConferenceLeaves.clear();
 365                for (Conversation conversation : account.pendingConferenceJoins) {
 366                    joinMuc(conversation);
 367                }
 368                account.pendingConferenceJoins.clear();
 369                scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
 370            } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) {
 371                resetSendingToWaiting(account);
 372                if (account.isEnabled() && isInLowPingTimeoutMode(account)) {
 373                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": went into offline state during low ping mode. reconnecting now");
 374                    reconnectAccount(account, true, false);
 375                } else {
 376                    int timeToReconnect = mRandom.nextInt(10) + 2;
 377                    scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
 378                }
 379            } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
 380                databaseBackend.updateAccount(account);
 381                reconnectAccount(account, true, false);
 382            } else if (account.getStatus() != Account.State.CONNECTING && account.getStatus() != Account.State.NO_INTERNET) {
 383                resetSendingToWaiting(account);
 384                if (connection != null && account.getStatus().isAttemptReconnect()) {
 385                    final int next = connection.getTimeToNextAttempt();
 386                    final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account);
 387                    if (next <= 0) {
 388                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error connecting account. reconnecting now. lowPingTimeout=" + Boolean.toString(lowPingTimeoutMode));
 389                        reconnectAccount(account, true, false);
 390                    } else {
 391                        final int attempt = connection.getAttempt() + 1;
 392                        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error connecting account. try again in " + next + "s for the " + attempt + " time. lowPingTimeout=" + Boolean.toString(lowPingTimeoutMode));
 393                        scheduleWakeUpCall(next, account.getUuid().hashCode());
 394                    }
 395                }
 396            }
 397            getNotificationService().updateErrorNotification();
 398        }
 399    };
 400    private OpenPgpServiceConnection pgpServiceConnection;
 401    private PgpEngine mPgpEngine = null;
 402    private WakeLock wakeLock;
 403    private PowerManager pm;
 404    private LruCache<String, Bitmap> mBitmapCache;
 405    private BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
 406    private BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
 407
 408    private static String generateFetchKey(Account account, final Avatar avatar) {
 409        return account.getJid().asBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum;
 410    }
 411
 412    private boolean isInLowPingTimeoutMode(Account account) {
 413        synchronized (mLowPingTimeoutMode) {
 414            return mLowPingTimeoutMode.contains(account.getJid().asBareJid());
 415        }
 416    }
 417
 418    public void startForcingForegroundNotification() {
 419        mForceForegroundService.set(true);
 420        toggleForegroundService();
 421    }
 422
 423    public void stopForcingForegroundNotification() {
 424        mForceForegroundService.set(false);
 425        toggleForegroundService();
 426    }
 427
 428    public boolean areMessagesInitialized() {
 429        return this.restoredFromDatabaseLatch.getCount() == 0;
 430    }
 431
 432    public PgpEngine getPgpEngine() {
 433        if (!Config.supportOpenPgp()) {
 434            return null;
 435        } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
 436            if (this.mPgpEngine == null) {
 437                this.mPgpEngine = new PgpEngine(new OpenPgpApi(
 438                        getApplicationContext(),
 439                        pgpServiceConnection.getService()), this);
 440            }
 441            return mPgpEngine;
 442        } else {
 443            return null;
 444        }
 445
 446    }
 447
 448    public OpenPgpApi getOpenPgpApi() {
 449        if (!Config.supportOpenPgp()) {
 450            return null;
 451        } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
 452            return new OpenPgpApi(this, pgpServiceConnection.getService());
 453        } else {
 454            return null;
 455        }
 456    }
 457
 458    public FileBackend getFileBackend() {
 459        return this.fileBackend;
 460    }
 461
 462    public AvatarService getAvatarService() {
 463        return this.mAvatarService;
 464    }
 465
 466    public void attachLocationToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
 467        int encryption = conversation.getNextEncryption();
 468        if (encryption == Message.ENCRYPTION_PGP) {
 469            encryption = Message.ENCRYPTION_DECRYPTED;
 470        }
 471        Message message = new Message(conversation, uri.toString(), encryption);
 472        if (conversation.getNextCounterpart() != null) {
 473            message.setCounterpart(conversation.getNextCounterpart());
 474        }
 475        if (encryption == Message.ENCRYPTION_DECRYPTED) {
 476            getPgpEngine().encrypt(message, callback);
 477        } else {
 478            sendMessage(message);
 479            callback.success(message);
 480        }
 481    }
 482
 483    public void attachFileToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback<Message> callback) {
 484        final Message message;
 485        if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 486            message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
 487        } else {
 488            message = new Message(conversation, "", conversation.getNextEncryption());
 489        }
 490        message.setCounterpart(conversation.getNextCounterpart());
 491        message.setType(Message.TYPE_FILE);
 492        final AttachFileToConversationRunnable runnable = new AttachFileToConversationRunnable(this, uri, type, message, callback);
 493        if (runnable.isVideoMessage()) {
 494            mVideoCompressionExecutor.execute(runnable);
 495        } else {
 496            mFileAddingExecutor.execute(runnable);
 497        }
 498    }
 499
 500    public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
 501        final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri);
 502        final String compressPictures = getCompressPicturesPreference();
 503
 504        if ("never".equals(compressPictures)
 505                || ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))
 506                || (mimeType != null && mimeType.endsWith("/gif"))
 507                || getFileBackend().unusualBounds(uri)) {
 508            Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": not compressing picture. sending as file");
 509            attachFileToConversation(conversation, uri, mimeType, callback);
 510            return;
 511        }
 512        final Message message;
 513        if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 514            message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
 515        } else {
 516            message = new Message(conversation, "", conversation.getNextEncryption());
 517        }
 518        message.setCounterpart(conversation.getNextCounterpart());
 519        message.setType(Message.TYPE_IMAGE);
 520        mFileAddingExecutor.execute(() -> {
 521            try {
 522                getFileBackend().copyImageToPrivateStorage(message, uri);
 523                if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 524                    final PgpEngine pgpEngine = getPgpEngine();
 525                    if (pgpEngine != null) {
 526                        pgpEngine.encrypt(message, callback);
 527                    } else if (callback != null) {
 528                        callback.error(R.string.unable_to_connect_to_keychain, null);
 529                    }
 530                } else {
 531                    sendMessage(message);
 532                    callback.success(message);
 533                }
 534            } catch (final FileBackend.FileCopyException e) {
 535                callback.error(e.getResId(), message);
 536            }
 537        });
 538    }
 539
 540    public Conversation find(Bookmark bookmark) {
 541        return find(bookmark.getAccount(), bookmark.getJid());
 542    }
 543
 544    public Conversation find(final Account account, final Jid jid) {
 545        return find(getConversations(), account, jid);
 546    }
 547
 548    public boolean isMuc(final Account account, final Jid jid) {
 549        final Conversation c = find(account, jid);
 550        return c != null && c.getMode() == Conversational.MODE_MULTI;
 551    }
 552
 553    public void search(List<String> term, OnSearchResultsAvailable onSearchResultsAvailable) {
 554        MessageSearchTask.search(this, term, onSearchResultsAvailable);
 555    }
 556
 557    @Override
 558    public int onStartCommand(Intent intent, int flags, int startId) {
 559        final String action = intent == null ? null : intent.getAction();
 560        final boolean needsForegroundService = intent != null && intent.getBooleanExtra(EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false);
 561        if (needsForegroundService) {
 562            Log.d(Config.LOGTAG,"toggle forced foreground service after receiving event (action="+action+")");
 563            toggleForegroundService(true);
 564        }
 565        String pushedAccountHash = null;
 566        boolean interactive = false;
 567        if (action != null) {
 568            final String uuid = intent.getStringExtra("uuid");
 569            switch (action) {
 570                case ConnectivityManager.CONNECTIVITY_ACTION:
 571                    if (hasInternetConnection() && Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
 572                        resetAllAttemptCounts(true, false);
 573                    }
 574                    break;
 575                case Intent.ACTION_SHUTDOWN:
 576                    logoutAndSave(true);
 577                    return START_NOT_STICKY;
 578                case ACTION_CLEAR_NOTIFICATION:
 579                    mNotificationExecutor.execute(() -> {
 580                        try {
 581                            final Conversation c = findConversationByUuid(uuid);
 582                            if (c != null) {
 583                                mNotificationService.clear(c);
 584                            } else {
 585                                mNotificationService.clear();
 586                            }
 587                            restoredFromDatabaseLatch.await();
 588
 589                        } catch (InterruptedException e) {
 590                            Log.d(Config.LOGTAG, "unable to process clear notification");
 591                        }
 592                    });
 593                    break;
 594                case ACTION_DISMISS_ERROR_NOTIFICATIONS:
 595                    dismissErrorNotifications();
 596                    break;
 597                case ACTION_TRY_AGAIN:
 598                    resetAllAttemptCounts(false, true);
 599                    interactive = true;
 600                    break;
 601                case ACTION_REPLY_TO_CONVERSATION:
 602                    Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
 603                    if (remoteInput == null) {
 604                        break;
 605                    }
 606                    final CharSequence body = remoteInput.getCharSequence("text_reply");
 607                    final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false);
 608                    if (body == null || body.length() <= 0) {
 609                        break;
 610                    }
 611                    mNotificationExecutor.execute(() -> {
 612                        try {
 613                            restoredFromDatabaseLatch.await();
 614                            final Conversation c = findConversationByUuid(uuid);
 615                            if (c != null) {
 616                                directReply(c, body.toString(), dismissNotification);
 617                            }
 618                        } catch (InterruptedException e) {
 619                            Log.d(Config.LOGTAG, "unable to process direct reply");
 620                        }
 621                    });
 622                    break;
 623                case ACTION_MARK_AS_READ:
 624                    mNotificationExecutor.execute(() -> {
 625                        final Conversation c = findConversationByUuid(uuid);
 626                        if (c == null) {
 627                            Log.d(Config.LOGTAG, "received mark read intent for unknown conversation (" + uuid + ")");
 628                            return;
 629                        }
 630                        try {
 631                            restoredFromDatabaseLatch.await();
 632                            sendReadMarker(c, null);
 633                        } catch (InterruptedException e) {
 634                            Log.d(Config.LOGTAG, "unable to process notification read marker for conversation " + c.getName());
 635                        }
 636
 637                    });
 638                    break;
 639                case ACTION_SNOOZE:
 640                    mNotificationExecutor.execute(() -> {
 641                        final Conversation c = findConversationByUuid(uuid);
 642                        if (c == null) {
 643                            Log.d(Config.LOGTAG, "received snooze intent for unknown conversation (" + uuid + ")");
 644                            return;
 645                        }
 646                        c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000);
 647                        mNotificationService.clear(c);
 648                        updateConversation(c);
 649                    });
 650                case AudioManager.RINGER_MODE_CHANGED_ACTION:
 651                case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED:
 652                    if (dndOnSilentMode()) {
 653                        refreshAllPresences();
 654                    }
 655                    break;
 656                case Intent.ACTION_SCREEN_ON:
 657                    deactivateGracePeriod();
 658                case Intent.ACTION_SCREEN_OFF:
 659                    if (awayWhenScreenOff()) {
 660                        refreshAllPresences();
 661                    }
 662                    break;
 663                case ACTION_FCM_TOKEN_REFRESH:
 664                    refreshAllFcmTokens();
 665                    break;
 666                case ACTION_IDLE_PING:
 667                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 668                        scheduleNextIdlePing();
 669                    }
 670                    break;
 671                case ACTION_FCM_MESSAGE_RECEIVED:
 672                    pushedAccountHash = intent.getStringExtra("account");
 673                    Log.d(Config.LOGTAG, "push message arrived in service. account=" + pushedAccountHash);
 674                    break;
 675                case Intent.ACTION_SEND:
 676                    Uri uri = intent.getData();
 677                    if (uri != null) {
 678                        Log.d(Config.LOGTAG, "received uri permission for " + uri.toString());
 679                    }
 680                    return START_STICKY;
 681            }
 682        }
 683        synchronized (this) {
 684            WakeLockHelper.acquire(wakeLock);
 685            boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action);
 686            HashSet<Account> pingCandidates = new HashSet<>();
 687            for (Account account : accounts) {
 688                pingNow |= processAccountState(account,
 689                        interactive,
 690                        "ui".equals(action),
 691                        CryptoHelper.getAccountFingerprint(account, PhoneHelper.getAndroidId(this)).equals(pushedAccountHash),
 692                        pingCandidates);
 693            }
 694            if (pingNow) {
 695                for (Account account : pingCandidates) {
 696                    final boolean lowTimeout = isInLowPingTimeoutMode(account);
 697                    account.getXmppConnection().sendPing();
 698                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + " send ping (action=" + action + ",lowTimeout=" + Boolean.toString(lowTimeout) + ")");
 699                    scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode());
 700                }
 701            }
 702            WakeLockHelper.release(wakeLock);
 703        }
 704        if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) {
 705            expireOldMessages();
 706        }
 707        return START_STICKY;
 708    }
 709
 710    private boolean processAccountState(Account account, boolean interactive, boolean isUiAction, boolean isAccountPushed, HashSet<Account> pingCandidates) {
 711        boolean pingNow = false;
 712        if (account.getStatus().isAttemptReconnect()) {
 713            if (!hasInternetConnection()) {
 714                account.setStatus(Account.State.NO_INTERNET);
 715                if (statusListener != null) {
 716                    statusListener.onStatusChanged(account);
 717                }
 718            } else {
 719                if (account.getStatus() == Account.State.NO_INTERNET) {
 720                    account.setStatus(Account.State.OFFLINE);
 721                    if (statusListener != null) {
 722                        statusListener.onStatusChanged(account);
 723                    }
 724                }
 725                if (account.getStatus() == Account.State.ONLINE) {
 726                    synchronized (mLowPingTimeoutMode) {
 727                        long lastReceived = account.getXmppConnection().getLastPacketReceived();
 728                        long lastSent = account.getXmppConnection().getLastPingSent();
 729                        long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
 730                        long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime();
 731                        int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().asBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000;
 732                        long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
 733                        if (lastSent > lastReceived) {
 734                            if (pingTimeoutIn < 0) {
 735                                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping timeout");
 736                                this.reconnectAccount(account, true, interactive);
 737                            } else {
 738                                int secs = (int) (pingTimeoutIn / 1000);
 739                                this.scheduleWakeUpCall(secs, account.getUuid().hashCode());
 740                            }
 741                        } else {
 742                            pingCandidates.add(account);
 743                            if (isAccountPushed) {
 744                                pingNow = true;
 745                                if (mLowPingTimeoutMode.add(account.getJid().asBareJid())) {
 746                                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": entering low ping timeout mode");
 747                                }
 748                            } else if (msToNextPing <= 0) {
 749                                pingNow = true;
 750                            } else {
 751                                this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
 752                                if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
 753                                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": leaving low ping timeout mode");
 754                                }
 755                            }
 756                        }
 757                    }
 758                } else if (account.getStatus() == Account.State.OFFLINE) {
 759                    reconnectAccount(account, true, interactive);
 760                } else if (account.getStatus() == Account.State.CONNECTING) {
 761                    long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
 762                    long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
 763                    long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
 764                    long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
 765                    if (timeout < 0) {
 766                        Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast=" + secondsSinceLastConnect + ")");
 767                        account.getXmppConnection().resetAttemptCount(false);
 768                        reconnectAccount(account, true, interactive);
 769                    } else if (discoTimeout < 0) {
 770                        account.getXmppConnection().sendDiscoTimeout();
 771                        scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode());
 772                    } else {
 773                        scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode());
 774                    }
 775                } else {
 776                    if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
 777                        reconnectAccount(account, true, interactive);
 778                    }
 779                }
 780            }
 781        }
 782        return pingNow;
 783    }
 784
 785    public boolean isDataSaverDisabled() {
 786        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 787            ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
 788            return !connectivityManager.isActiveNetworkMetered()
 789                    || connectivityManager.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
 790        } else {
 791            return true;
 792        }
 793    }
 794
 795    private void directReply(Conversation conversation, String body, final boolean dismissAfterReply) {
 796        Message message = new Message(conversation, body, conversation.getNextEncryption());
 797        message.markUnread();
 798        if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 799            getPgpEngine().encrypt(message, new UiCallback<Message>() {
 800                @Override
 801                public void success(Message message) {
 802                    if (dismissAfterReply) {
 803                        markRead((Conversation) message.getConversation(), true);
 804                    } else {
 805                        mNotificationService.pushFromDirectReply(message);
 806                    }
 807                }
 808
 809                @Override
 810                public void error(int errorCode, Message object) {
 811
 812                }
 813
 814                @Override
 815                public void userInputRequried(PendingIntent pi, Message object) {
 816
 817                }
 818            });
 819        } else {
 820            sendMessage(message);
 821            if (dismissAfterReply) {
 822                markRead(conversation, true);
 823            } else {
 824                mNotificationService.pushFromDirectReply(message);
 825            }
 826        }
 827    }
 828
 829    private boolean dndOnSilentMode() {
 830        return getBooleanPreference(SettingsActivity.DND_ON_SILENT_MODE, R.bool.dnd_on_silent_mode);
 831    }
 832
 833    private boolean manuallyChangePresence() {
 834        return getBooleanPreference(SettingsActivity.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
 835    }
 836
 837    private boolean treatVibrateAsSilent() {
 838        return getBooleanPreference(SettingsActivity.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent);
 839    }
 840
 841    private boolean awayWhenScreenOff() {
 842        return getBooleanPreference(SettingsActivity.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off);
 843    }
 844
 845    private String getCompressPicturesPreference() {
 846        return getPreferences().getString("picture_compression", getResources().getString(R.string.picture_compression));
 847    }
 848
 849    private Presence.Status getTargetPresence() {
 850        if (dndOnSilentMode() && isPhoneSilenced()) {
 851            return Presence.Status.DND;
 852        } else if (awayWhenScreenOff() && !isInteractive()) {
 853            return Presence.Status.AWAY;
 854        } else {
 855            return Presence.Status.ONLINE;
 856        }
 857    }
 858
 859    @SuppressLint("NewApi")
 860    @SuppressWarnings("deprecation")
 861    public boolean isInteractive() {
 862        try {
 863            final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 864
 865            final boolean isScreenOn;
 866            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
 867                isScreenOn = pm.isScreenOn();
 868            } else {
 869                isScreenOn = pm.isInteractive();
 870            }
 871            return isScreenOn;
 872        } catch (RuntimeException e) {
 873            return false;
 874        }
 875    }
 876
 877    private boolean isPhoneSilenced() {
 878        final boolean notificationDnd;
 879        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 880            final NotificationManager notificationManager = getSystemService(NotificationManager.class);
 881            final int filter = notificationManager == null ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN : notificationManager.getCurrentInterruptionFilter();
 882            notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY;
 883        } else {
 884            notificationDnd = false;
 885        }
 886        final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
 887        final int ringerMode = audioManager == null ? AudioManager.RINGER_MODE_NORMAL : audioManager.getRingerMode();
 888        try {
 889            if (treatVibrateAsSilent()) {
 890                return notificationDnd || ringerMode != AudioManager.RINGER_MODE_NORMAL;
 891            } else {
 892                return notificationDnd || ringerMode == AudioManager.RINGER_MODE_SILENT;
 893            }
 894        } catch (Throwable throwable) {
 895            Log.d(Config.LOGTAG, "platform bug in isPhoneSilenced (" + throwable.getMessage() + ")");
 896            return notificationDnd;
 897        }
 898    }
 899
 900    private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) {
 901        Log.d(Config.LOGTAG, "resetting all attempt counts");
 902        for (Account account : accounts) {
 903            if (account.hasErrorStatus() || reallyAll) {
 904                final XmppConnection connection = account.getXmppConnection();
 905                if (connection != null) {
 906                    connection.resetAttemptCount(retryImmediately);
 907                }
 908            }
 909            if (account.setShowErrorNotification(true)) {
 910                databaseBackend.updateAccount(account);
 911            }
 912        }
 913        mNotificationService.updateErrorNotification();
 914    }
 915
 916    private void dismissErrorNotifications() {
 917        for (final Account account : this.accounts) {
 918            if (account.hasErrorStatus()) {
 919                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": dismissing error notification");
 920                if (account.setShowErrorNotification(false)) {
 921                    databaseBackend.updateAccount(account);
 922                }
 923            }
 924        }
 925    }
 926
 927    private void expireOldMessages() {
 928        expireOldMessages(false);
 929    }
 930
 931    public void expireOldMessages(final boolean resetHasMessagesLeftOnServer) {
 932        mLastExpiryRun.set(SystemClock.elapsedRealtime());
 933        mDatabaseWriterExecutor.execute(() -> {
 934            long timestamp = getAutomaticMessageDeletionDate();
 935            if (timestamp > 0) {
 936                databaseBackend.expireOldMessages(timestamp);
 937                synchronized (XmppConnectionService.this.conversations) {
 938                    for (Conversation conversation : XmppConnectionService.this.conversations) {
 939                        conversation.expireOldMessages(timestamp);
 940                        if (resetHasMessagesLeftOnServer) {
 941                            conversation.messagesLoaded.set(true);
 942                            conversation.setHasMessagesLeftOnServer(true);
 943                        }
 944                    }
 945                }
 946                updateConversationUi();
 947            }
 948        });
 949    }
 950
 951    public boolean hasInternetConnection() {
 952        final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
 953        try {
 954            final NetworkInfo activeNetwork = cm == null ? null : cm.getActiveNetworkInfo();
 955            return activeNetwork != null && (activeNetwork.isConnected() || activeNetwork.getType() == ConnectivityManager.TYPE_ETHERNET);
 956        } catch (RuntimeException e) {
 957            Log.d(Config.LOGTAG, "unable to check for internet connection", e);
 958            return true; //if internet connection can not be checked it is probably best to just try
 959        }
 960    }
 961
 962    @SuppressLint("TrulyRandom")
 963    @Override
 964    public void onCreate() {
 965        OmemoSetting.load(this);
 966        ExceptionHelper.init(getApplicationContext());
 967        try {
 968            Security.insertProviderAt(Conscrypt.newProvider(), 1);
 969        } catch (Throwable throwable) {
 970            Log.e(Config.LOGTAG,"unable to initialize security provider", throwable);
 971        }
 972        Resolver.init(this);
 973        this.mRandom = new SecureRandom();
 974        updateMemorizingTrustmanager();
 975        if (Compatibility.runsTwentySix()) {
 976            mNotificationService.initializeChannels();
 977        }
 978        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
 979        final int cacheSize = maxMemory / 8;
 980        this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
 981            @Override
 982            protected int sizeOf(final String key, final Bitmap bitmap) {
 983                return bitmap.getByteCount() / 1024;
 984            }
 985        };
 986        if (mLastActivity == 0) {
 987            mLastActivity = getPreferences().getLong(SETTING_LAST_ACTIVITY_TS, System.currentTimeMillis());
 988        }
 989
 990        Log.d(Config.LOGTAG, "initializing database...");
 991        this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
 992        Log.d(Config.LOGTAG, "restoring accounts...");
 993        this.accounts = databaseBackend.getAccounts();
 994        final SharedPreferences.Editor editor = getPreferences().edit();
 995        if (this.accounts.size() == 0 && Arrays.asList("Sony", "Sony Ericsson").contains(Build.MANUFACTURER)) {
 996            editor.putBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE, true);
 997            Log.d(Config.LOGTAG, Build.MANUFACTURER + " is on blacklist. enabling foreground service");
 998        }
 999        editor.putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts()).apply();
1000        editor.apply();
1001
1002        restoreFromDatabase();
1003
1004        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
1005            startContactObserver();
1006        }
1007        if (Compatibility.hasStoragePermission(this)) {
1008            Log.d(Config.LOGTAG, "starting file observer");
1009            new Thread(fileObserver::startWatching).start();
1010            mFileAddingExecutor.execute(this::checkForDeletedFiles);
1011        }
1012        if (Config.supportOpenPgp()) {
1013            this.pgpServiceConnection = new OpenPgpServiceConnection(this, "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() {
1014                @Override
1015                public void onBound(IOpenPgpService2 service) {
1016                    for (Account account : accounts) {
1017                        final PgpDecryptionService pgp = account.getPgpDecryptionService();
1018                        if (pgp != null) {
1019                            pgp.continueDecryption(true);
1020                        }
1021                    }
1022                }
1023
1024                @Override
1025                public void onError(Exception e) {
1026                }
1027            });
1028            this.pgpServiceConnection.bindToService();
1029        }
1030
1031        this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
1032        this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service");
1033
1034        toggleForegroundService();
1035        updateUnreadCountBadge();
1036        toggleScreenEventReceiver();
1037        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1038            scheduleNextIdlePing();
1039            IntentFilter intentFilter = new IntentFilter();
1040            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1041                intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
1042            }
1043            intentFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
1044            registerReceiver(this.mInternalEventReceiver, intentFilter);
1045        }
1046    }
1047
1048    private void checkForDeletedFiles() {
1049        final List<String> deletedUuids = new ArrayList<>();
1050        final List<DatabaseBackend.FilePath> relativeFilePaths = databaseBackend.getAllNonDeletedFilePath();
1051        for(final DatabaseBackend.FilePath filePath : relativeFilePaths) {
1052            final File file = fileBackend.getFileForPath(filePath.path);
1053            if (!file.exists()) {
1054                deletedUuids.add(filePath.uuid.toString());
1055            }
1056        }
1057        Log.d(Config.LOGTAG,"found "+deletedUuids.size()+" deleted files on start up. total="+relativeFilePaths.size());
1058        if (deletedUuids.size() > 0) {
1059            databaseBackend.markFileAsDeleted(deletedUuids);
1060            markUuidsAsDeletedFiles(deletedUuids);
1061        }
1062    }
1063
1064    public void startContactObserver() {
1065        getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, new ContentObserver(null) {
1066            @Override
1067            public void onChange(boolean selfChange) {
1068                super.onChange(selfChange);
1069                if (restoredFromDatabaseLatch.getCount() == 0) {
1070                    loadPhoneContacts();
1071                }
1072            }
1073        });
1074    }
1075
1076    @Override
1077    public void onTrimMemory(int level) {
1078        super.onTrimMemory(level);
1079        if (level >= TRIM_MEMORY_COMPLETE) {
1080            Log.d(Config.LOGTAG, "clear cache due to low memory");
1081            getBitmapCache().evictAll();
1082        }
1083    }
1084
1085    @Override
1086    public void onDestroy() {
1087        try {
1088            unregisterReceiver(this.mInternalEventReceiver);
1089        } catch (IllegalArgumentException e) {
1090            //ignored
1091        }
1092        fileObserver.stopWatching();
1093        super.onDestroy();
1094    }
1095
1096    public void restartFileObserver() {
1097        Log.d(Config.LOGTAG, "restarting file observer");
1098        mFileAddingExecutor.execute(this::checkForDeletedFiles);
1099        new Thread(fileObserver::restartWatching).start();
1100    }
1101
1102    public void toggleScreenEventReceiver() {
1103        if (awayWhenScreenOff() && !manuallyChangePresence()) {
1104            final IntentFilter filter = new IntentFilter();
1105            filter.addAction(Intent.ACTION_SCREEN_ON);
1106            filter.addAction(Intent.ACTION_SCREEN_OFF);
1107            registerReceiver(this.mInternalScreenEventReceiver, filter);
1108        } else {
1109            try {
1110                unregisterReceiver(this.mInternalScreenEventReceiver);
1111            } catch (IllegalArgumentException e) {
1112                //ignored
1113            }
1114        }
1115    }
1116
1117    public void toggleForegroundService() {
1118        toggleForegroundService(false);
1119    }
1120
1121    private void toggleForegroundService(boolean force) {
1122        final boolean status;
1123        if (force || mForceForegroundService.get() || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) {
1124            startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
1125            status = true;
1126        } else {
1127            stopForeground(true);
1128            status = false;
1129        }
1130        mNotificationService.dismissForcedForegroundNotification(); //if the channel was changed the previous call might fail
1131        Log.d(Config.LOGTAG,"ForegroundService: "+(status?"on":"off"));
1132    }
1133
1134    public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() {
1135        return !mForceForegroundService.get() && Compatibility.keepForegroundService(this) && hasEnabledAccounts();
1136    }
1137
1138    @Override
1139    public void onTaskRemoved(final Intent rootIntent) {
1140        super.onTaskRemoved(rootIntent);
1141        if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts()) || mForceForegroundService.get()) {
1142            Log.d(Config.LOGTAG, "ignoring onTaskRemoved because foreground service is activated");
1143        } else {
1144            this.logoutAndSave(false);
1145        }
1146    }
1147
1148    private void logoutAndSave(boolean stop) {
1149        int activeAccounts = 0;
1150        for (final Account account : accounts) {
1151            if (account.getStatus() != Account.State.DISABLED) {
1152                databaseBackend.writeRoster(account.getRoster());
1153                activeAccounts++;
1154            }
1155            if (account.getXmppConnection() != null) {
1156                new Thread(() -> disconnect(account, false)).start();
1157            }
1158        }
1159        if (stop || activeAccounts == 0) {
1160            Log.d(Config.LOGTAG, "good bye");
1161            stopSelf();
1162        }
1163    }
1164
1165    public void scheduleWakeUpCall(int seconds, int requestCode) {
1166        final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000;
1167        final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
1168        if (alarmManager == null) {
1169            return;
1170        }
1171        final Intent intent = new Intent(this, EventReceiver.class);
1172        intent.setAction("ping");
1173        try {
1174            PendingIntent pendingIntent = PendingIntent.getBroadcast(this, requestCode, intent, 0);
1175            alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
1176        } catch (RuntimeException e) {
1177            Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e);
1178        }
1179    }
1180
1181    @TargetApi(Build.VERSION_CODES.M)
1182    private void scheduleNextIdlePing() {
1183        final long timeToWake = SystemClock.elapsedRealtime() + (Config.IDLE_PING_INTERVAL * 1000);
1184        final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
1185        if (alarmManager == null) {
1186            return;
1187        }
1188        final Intent intent = new Intent(this, EventReceiver.class);
1189        intent.setAction(ACTION_IDLE_PING);
1190        try {
1191            PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
1192            alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
1193        } catch (RuntimeException e) {
1194            Log.d(Config.LOGTAG, "unable to schedule alarm for idle ping", e);
1195        }
1196    }
1197
1198    public XmppConnection createConnection(final Account account) {
1199        final XmppConnection connection = new XmppConnection(account, this);
1200        connection.setOnMessagePacketReceivedListener(this.mMessageParser);
1201        connection.setOnStatusChangedListener(this.statusListener);
1202        connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
1203        connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
1204        connection.setOnJinglePacketReceivedListener(this.jingleListener);
1205        connection.setOnBindListener(this.mOnBindListener);
1206        connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
1207        connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
1208        connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService);
1209        AxolotlService axolotlService = account.getAxolotlService();
1210        if (axolotlService != null) {
1211            connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
1212        }
1213        return connection;
1214    }
1215
1216    public void sendChatState(Conversation conversation) {
1217        if (sendChatStates()) {
1218            MessagePacket packet = mMessageGenerator.generateChatState(conversation);
1219            sendMessagePacket(conversation.getAccount(), packet);
1220        }
1221    }
1222
1223    private void sendFileMessage(final Message message, final boolean delay) {
1224        Log.d(Config.LOGTAG, "send file message");
1225        final Account account = message.getConversation().getAccount();
1226        if (account.httpUploadAvailable(fileBackend.getFile(message, false).getSize())
1227                || message.getConversation().getMode() == Conversation.MODE_MULTI) {
1228            mHttpConnectionManager.createNewUploadConnection(message, delay);
1229        } else {
1230            mJingleConnectionManager.createNewConnection(message);
1231        }
1232    }
1233
1234    public void sendMessage(final Message message) {
1235        sendMessage(message, false, false);
1236    }
1237
1238    private void sendMessage(final Message message, final boolean resend, final boolean delay) {
1239        final Account account = message.getConversation().getAccount();
1240        if (account.setShowErrorNotification(true)) {
1241            databaseBackend.updateAccount(account);
1242            mNotificationService.updateErrorNotification();
1243        }
1244        final Conversation conversation = (Conversation) message.getConversation();
1245        account.deactivateGracePeriod();
1246
1247
1248        if (QuickConversationsService.isQuicksy() && conversation.getMode() == Conversation.MODE_SINGLE) {
1249            final Contact contact = conversation.getContact();
1250            if (!contact.showInRoster() && contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) {
1251                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": adding "+contact.getJid()+" on sending message");
1252                createContact(contact, true);
1253            }
1254        }
1255
1256        MessagePacket packet = null;
1257        final boolean addToConversation = (conversation.getMode() != Conversation.MODE_MULTI
1258                || !Patches.BAD_MUC_REFLECTION.contains(account.getServerIdentity()))
1259                && !message.edited();
1260        boolean saveInDb = addToConversation;
1261        message.setStatus(Message.STATUS_WAITING);
1262
1263        if (account.isOnlineAndConnected()) {
1264            switch (message.getEncryption()) {
1265                case Message.ENCRYPTION_NONE:
1266                    if (message.needsUploading()) {
1267                        if (account.httpUploadAvailable(fileBackend.getFile(message, false).getSize())
1268                                || conversation.getMode() == Conversation.MODE_MULTI
1269                                || message.fixCounterpart()) {
1270                            this.sendFileMessage(message, delay);
1271                        } else {
1272                            break;
1273                        }
1274                    } else {
1275                        packet = mMessageGenerator.generateChat(message);
1276                    }
1277                    break;
1278                case Message.ENCRYPTION_PGP:
1279                case Message.ENCRYPTION_DECRYPTED:
1280                    if (message.needsUploading()) {
1281                        if (account.httpUploadAvailable(fileBackend.getFile(message, false).getSize())
1282                                || conversation.getMode() == Conversation.MODE_MULTI
1283                                || message.fixCounterpart()) {
1284                            this.sendFileMessage(message, delay);
1285                        } else {
1286                            break;
1287                        }
1288                    } else {
1289                        packet = mMessageGenerator.generatePgpChat(message);
1290                    }
1291                    break;
1292                case Message.ENCRYPTION_AXOLOTL:
1293                    message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
1294                    if (message.needsUploading()) {
1295                        if (account.httpUploadAvailable(fileBackend.getFile(message, false).getSize())
1296                                || conversation.getMode() == Conversation.MODE_MULTI
1297                                || message.fixCounterpart()) {
1298                            this.sendFileMessage(message, delay);
1299                        } else {
1300                            break;
1301                        }
1302                    } else {
1303                        XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message);
1304                        if (axolotlMessage == null) {
1305                            account.getAxolotlService().preparePayloadMessage(message, delay);
1306                        } else {
1307                            packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
1308                        }
1309                    }
1310                    break;
1311
1312            }
1313            if (packet != null) {
1314                if (account.getXmppConnection().getFeatures().sm()
1315                        || (conversation.getMode() == Conversation.MODE_MULTI && message.getCounterpart().isBareJid())) {
1316                    message.setStatus(Message.STATUS_UNSEND);
1317                } else {
1318                    message.setStatus(Message.STATUS_SEND);
1319                }
1320            }
1321        } else {
1322            switch (message.getEncryption()) {
1323                case Message.ENCRYPTION_DECRYPTED:
1324                    if (!message.needsUploading()) {
1325                        String pgpBody = message.getEncryptedBody();
1326                        String decryptedBody = message.getBody();
1327                        message.setBody(pgpBody); //TODO might throw NPE
1328                        message.setEncryption(Message.ENCRYPTION_PGP);
1329                        if (message.edited()) {
1330                            message.setBody(decryptedBody);
1331                            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
1332                            if (!databaseBackend.updateMessage(message, message.getEditedId())) {
1333                                Log.e(Config.LOGTAG,"error updated message in DB after edit");
1334                            }
1335                            updateConversationUi();
1336                            return;
1337                        } else {
1338                            databaseBackend.createMessage(message);
1339                            saveInDb = false;
1340                            message.setBody(decryptedBody);
1341                            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
1342                        }
1343                    }
1344                    break;
1345                case Message.ENCRYPTION_AXOLOTL:
1346                    message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
1347                    break;
1348            }
1349        }
1350
1351
1352        boolean mucMessage = conversation.getMode() == Conversation.MODE_MULTI && message.getType() != Message.TYPE_PRIVATE;
1353        if (mucMessage) {
1354            message.setCounterpart(conversation.getMucOptions().getSelf().getFullJid());
1355        }
1356
1357        if (resend) {
1358            if (packet != null && addToConversation) {
1359                if (account.getXmppConnection().getFeatures().sm() || mucMessage) {
1360                    markMessage(message, Message.STATUS_UNSEND);
1361                } else {
1362                    markMessage(message, Message.STATUS_SEND);
1363                }
1364            }
1365        } else {
1366            if (addToConversation) {
1367                conversation.add(message);
1368            }
1369            if (saveInDb) {
1370                databaseBackend.createMessage(message);
1371            } else if (message.edited()) {
1372                if (!databaseBackend.updateMessage(message, message.getEditedId())) {
1373                    Log.e(Config.LOGTAG,"error updated message in DB after edit");
1374                }
1375            }
1376            updateConversationUi();
1377        }
1378        if (packet != null) {
1379            if (delay) {
1380                mMessageGenerator.addDelay(packet, message.getTimeSent());
1381            }
1382            if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
1383                if (this.sendChatStates()) {
1384                    packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
1385                }
1386            }
1387            sendMessagePacket(account, packet);
1388        }
1389    }
1390
1391    private void sendUnsentMessages(final Conversation conversation) {
1392        conversation.findWaitingMessages(message -> resendMessage(message, true));
1393    }
1394
1395    public void resendMessage(final Message message, final boolean delay) {
1396        sendMessage(message, true, delay);
1397    }
1398
1399    public void fetchRosterFromServer(final Account account) {
1400        final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
1401        if (!"".equals(account.getRosterVersion())) {
1402            Log.d(Config.LOGTAG, account.getJid().asBareJid()
1403                    + ": fetching roster version " + account.getRosterVersion());
1404        } else {
1405            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching roster");
1406        }
1407        iqPacket.query(Namespace.ROSTER).setAttribute("ver", account.getRosterVersion());
1408        sendIqPacket(account, iqPacket, mIqParser);
1409    }
1410
1411    public void fetchBookmarks(final Account account) {
1412        final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
1413        final Element query = iqPacket.query("jabber:iq:private");
1414        query.addChild("storage", Namespace.BOOKMARKS);
1415        final OnIqPacketReceived callback = (a, response) -> {
1416            if (response.getType() == IqPacket.TYPE.RESULT) {
1417                final Element query1 = response.query();
1418                final Element storage = query1.findChild("storage", "storage:bookmarks");
1419                processBookmarks(a, storage, false);
1420            } else {
1421                Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": could not fetch bookmarks");
1422            }
1423        };
1424        sendIqPacket(account, iqPacket, callback);
1425    }
1426
1427    public void processBookmarks(Account account, Element storage, final boolean pep) {
1428        final Set<Jid> previousBookmarks = account.getBookmarkedJids();
1429        final HashMap<Jid, Bookmark> bookmarks = new HashMap<>();
1430        final boolean synchronizeWithBookmarks = synchronizeWithBookmarks();
1431        if (storage != null) {
1432            for (final Element item : storage.getChildren()) {
1433                if (item.getName().equals("conference")) {
1434                    final Bookmark bookmark = Bookmark.parse(item, account);
1435                    Bookmark old = bookmarks.put(bookmark.getJid(), bookmark);
1436                    if (old != null && old.getBookmarkName() != null && bookmark.getBookmarkName() == null) {
1437                        bookmark.setBookmarkName(old.getBookmarkName());
1438                    }
1439                    if (bookmark.getJid() == null) {
1440                        continue;
1441                    }
1442                    previousBookmarks.remove(bookmark.getJid().asBareJid());
1443                    Conversation conversation = find(bookmark);
1444                    if (conversation != null) {
1445                        if (conversation.getMode() != Conversation.MODE_MULTI) {
1446                            continue;
1447                        }
1448                        bookmark.setConversation(conversation);
1449                        if (pep && synchronizeWithBookmarks && !bookmark.autojoin()) {
1450                            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": archiving conference ("+conversation.getJid()+") after receiving pep");
1451                            archiveConversation(conversation, false);
1452                        }
1453                    } else if (synchronizeWithBookmarks && bookmark.autojoin()) {
1454                        conversation = findOrCreateConversation(account, bookmark.getFullJid(), true, true, false);
1455                        bookmark.setConversation(conversation);
1456                    }
1457                }
1458            }
1459            if (pep && synchronizeWithBookmarks) {
1460                Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + previousBookmarks.size() + " bookmarks have been removed");
1461                for (Jid jid : previousBookmarks) {
1462                    final Conversation conversation = find(account, jid);
1463                    if (conversation != null && conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) {
1464                        Log.d(Config.LOGTAG,account.getJid().asBareJid()+": archiving destroyed conference ("+conversation.getJid()+") after receiving pep");
1465                        archiveConversation(conversation, false);
1466                    }
1467                }
1468            }
1469        }
1470        account.setBookmarks(new CopyOnWriteArrayList<>(bookmarks.values()));
1471    }
1472
1473    public void pushBookmarks(Account account) {
1474        if (account.getXmppConnection().getFeatures().bookmarksConversion()) {
1475            pushBookmarksPep(account);
1476        } else {
1477            pushBookmarksPrivateXml(account);
1478        }
1479    }
1480
1481    private void pushBookmarksPrivateXml(Account account) {
1482        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml");
1483        IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
1484        Element query = iqPacket.query("jabber:iq:private");
1485        Element storage = query.addChild("storage", "storage:bookmarks");
1486        for (Bookmark bookmark : account.getBookmarks()) {
1487            storage.addChild(bookmark);
1488        }
1489        sendIqPacket(account, iqPacket, mDefaultIqHandler);
1490    }
1491
1492    private void pushBookmarksPep(Account account) {
1493        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via pep");
1494        Element storage = new Element("storage", "storage:bookmarks");
1495        for (Bookmark bookmark : account.getBookmarks()) {
1496            storage.addChild(bookmark);
1497        }
1498        pushNodeAndEnforcePublishOptions(account,Namespace.BOOKMARKS,storage, PublishOptions.persistentWhitelistAccess());
1499
1500    }
1501
1502
1503    private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final Bundle options) {
1504        pushNodeAndEnforcePublishOptions(account, node, element, options, true);
1505
1506    }
1507
1508	private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final Bundle options, final boolean retry) {
1509        final IqPacket packet = mIqGenerator.publishElement(node, element, options);
1510        sendIqPacket(account, packet, (a, response) -> {
1511            if (response.getType() == IqPacket.TYPE.RESULT) {
1512                return;
1513            }
1514            if (retry && PublishOptions.preconditionNotMet(response)) {
1515                pushNodeConfiguration(account, node, options, new OnConfigurationPushed() {
1516                    @Override
1517                    public void onPushSucceeded() {
1518                        pushNodeAndEnforcePublishOptions(account, node, element, options, false);
1519                    }
1520
1521                    @Override
1522                    public void onPushFailed() {
1523                        Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to push node configuration ("+node+")");
1524                    }
1525                });
1526            } else {
1527                Log.d(Config.LOGTAG,account.getJid().asBareJid()+": error publishing bookmarks (retry="+Boolean.toString(retry)+") "+response);
1528            }
1529        });
1530    }
1531
1532	private void restoreFromDatabase() {
1533		synchronized (this.conversations) {
1534			final Map<String, Account> accountLookupTable = new Hashtable<>();
1535			for (Account account : this.accounts) {
1536				accountLookupTable.put(account.getUuid(), account);
1537			}
1538			Log.d(Config.LOGTAG, "restoring conversations...");
1539			final long startTimeConversationsRestore = SystemClock.elapsedRealtime();
1540			this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
1541			for (Iterator<Conversation> iterator = conversations.listIterator(); iterator.hasNext(); ) {
1542				Conversation conversation = iterator.next();
1543				Account account = accountLookupTable.get(conversation.getAccountUuid());
1544				if (account != null) {
1545					conversation.setAccount(account);
1546				} else {
1547					Log.e(Config.LOGTAG, "unable to restore Conversations with " + conversation.getJid());
1548					iterator.remove();
1549				}
1550			}
1551			long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore;
1552			Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms");
1553			Runnable runnable = () -> {
1554				long deletionDate = getAutomaticMessageDeletionDate();
1555				mLastExpiryRun.set(SystemClock.elapsedRealtime());
1556				if (deletionDate > 0) {
1557					Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate));
1558					databaseBackend.expireOldMessages(deletionDate);
1559				}
1560				Log.d(Config.LOGTAG, "restoring roster...");
1561				for (Account account : accounts) {
1562					databaseBackend.readRoster(account.getRoster());
1563					account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage
1564				}
1565				getBitmapCache().evictAll();
1566				loadPhoneContacts();
1567				Log.d(Config.LOGTAG, "restoring messages...");
1568				final long startMessageRestore = SystemClock.elapsedRealtime();
1569				final Conversation quickLoad = QuickLoader.get(this.conversations);
1570				if (quickLoad != null) {
1571					restoreMessages(quickLoad);
1572					updateConversationUi();
1573					final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
1574					Log.d(Config.LOGTAG,"quickly restored "+quickLoad.getName()+" after " + diffMessageRestore + "ms");
1575				}
1576				for (Conversation conversation : this.conversations) {
1577					if (quickLoad != conversation) {
1578						restoreMessages(conversation);
1579					}
1580				}
1581				mNotificationService.finishBacklog(false);
1582				restoredFromDatabaseLatch.countDown();
1583				final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
1584				Log.d(Config.LOGTAG, "finished restoring messages in " + diffMessageRestore + "ms");
1585				updateConversationUi();
1586			};
1587			mDatabaseReaderExecutor.execute(runnable); //will contain one write command (expiry) but that's fine
1588		}
1589	}
1590
1591	private void restoreMessages(Conversation conversation) {
1592		conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
1593		conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
1594		conversation.findUnreadMessages(message -> mNotificationService.pushFromBacklog(message));
1595	}
1596
1597	public void loadPhoneContacts() {
1598        mContactMergerExecutor.execute(() -> {
1599            Map<Jid, JabberIdContact> contacts = JabberIdContact.load(this);
1600            Log.d(Config.LOGTAG, "start merging phone contacts with roster");
1601            for (Account account : accounts) {
1602                List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts(JabberIdContact.class);
1603                for (JabberIdContact jidContact : contacts.values()) {
1604                    final Contact contact = account.getRoster().getContact(jidContact.getJid());
1605                    boolean needsCacheClean = contact.setPhoneContact(jidContact);
1606                    if (needsCacheClean) {
1607                        getAvatarService().clear(contact);
1608                    }
1609                    withSystemAccounts.remove(contact);
1610                }
1611                for (Contact contact : withSystemAccounts) {
1612                    boolean needsCacheClean = contact.unsetPhoneContact(JabberIdContact.class);
1613                    if (needsCacheClean) {
1614                        getAvatarService().clear(contact);
1615                    }
1616                }
1617            }
1618            Log.d(Config.LOGTAG, "finished merging phone contacts");
1619            mShortcutService.refresh(mInitialAddressbookSyncCompleted.compareAndSet(false, true));
1620            updateRosterUi();
1621            mQuickConversationsService.considerSync();
1622        });
1623	}
1624
1625
1626	public void syncRoster(final Account account) {
1627		mRosterSyncTaskManager.execute(account, () -> databaseBackend.writeRoster(account.getRoster()));
1628	}
1629
1630	public List<Conversation> getConversations() {
1631		return this.conversations;
1632	}
1633
1634	private void markFileDeleted(final String path) {
1635        final File file = new File(path);
1636        final boolean isInternalFile = fileBackend.isInternalFile(file);
1637        final List<String> uuids = databaseBackend.markFileAsDeleted(file, isInternalFile);
1638		Log.d(Config.LOGTAG, "deleted file " + path+" internal="+isInternalFile+", database hits="+uuids.size());
1639		markUuidsAsDeletedFiles(uuids);
1640	}
1641
1642	private void markUuidsAsDeletedFiles(List<String> uuids) {
1643        boolean deleted = false;
1644        for (Conversation conversation : getConversations()) {
1645            deleted |= conversation.markAsDeleted(uuids);
1646        }
1647        if (deleted) {
1648            updateConversationUi();
1649        }
1650    }
1651
1652	public void populateWithOrderedConversations(final List<Conversation> list) {
1653		populateWithOrderedConversations(list, true);
1654	}
1655
1656	public void populateWithOrderedConversations(final List<Conversation> list, boolean includeNoFileUpload) {
1657		list.clear();
1658		if (includeNoFileUpload) {
1659			list.addAll(getConversations());
1660		} else {
1661			for (Conversation conversation : getConversations()) {
1662				if (conversation.getMode() == Conversation.MODE_SINGLE
1663						|| (conversation.getAccount().httpUploadAvailable() && conversation.getMucOptions().participating())) {
1664					list.add(conversation);
1665				}
1666			}
1667		}
1668		try {
1669			Collections.sort(list);
1670		} catch (IllegalArgumentException e) {
1671			//ignore
1672		}
1673	}
1674
1675	public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
1676		if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback)) {
1677			return;
1678		} else if (timestamp == 0) {
1679			return;
1680		}
1681		Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp));
1682		final Runnable runnable = () -> {
1683			final Account account = conversation.getAccount();
1684			List<Message> messages = databaseBackend.getMessages(conversation, 50, timestamp);
1685			if (messages.size() > 0) {
1686				conversation.addAll(0, messages);
1687				callback.onMoreMessagesLoaded(messages.size(), conversation);
1688			} else if (conversation.hasMessagesLeftOnServer()
1689					&& account.isOnlineAndConnected()
1690					&& conversation.getLastClearHistory().getTimestamp() == 0) {
1691				final boolean mamAvailable;
1692				if (conversation.getMode() == Conversation.MODE_SINGLE) {
1693					mamAvailable = account.getXmppConnection().getFeatures().mam() && !conversation.getContact().isBlocked();
1694				} else {
1695					mamAvailable = conversation.getMucOptions().mamSupport();
1696				}
1697				if (mamAvailable) {
1698					MessageArchiveService.Query query = getMessageArchiveService().query(conversation, new MamReference(0), timestamp, false);
1699					if (query != null) {
1700						query.setCallback(callback);
1701						callback.informUser(R.string.fetching_history_from_server);
1702					} else {
1703						callback.informUser(R.string.not_fetching_history_retention_period);
1704					}
1705
1706				}
1707			}
1708		};
1709		mDatabaseReaderExecutor.execute(runnable);
1710	}
1711
1712	public List<Account> getAccounts() {
1713		return this.accounts;
1714	}
1715
1716
1717    /**
1718     * This will find all conferences with the contact as member and also the conference that is the contact (that 'fake' contact is used to store the avatar)
1719     */
1720	public List<Conversation> findAllConferencesWith(Contact contact) {
1721		ArrayList<Conversation> results = new ArrayList<>();
1722		for (final Conversation c : conversations) {
1723			if (c.getMode() == Conversation.MODE_MULTI && (c.getJid().asBareJid().equals(contact.getJid().asBareJid()) || c.getMucOptions().isContactInRoom(contact))) {
1724			    results.add(c);
1725			}
1726		}
1727		return results;
1728	}
1729
1730	public Conversation find(final Iterable<Conversation> haystack, final Contact contact) {
1731		for (final Conversation conversation : haystack) {
1732			if (conversation.getContact() == contact) {
1733				return conversation;
1734			}
1735		}
1736		return null;
1737	}
1738
1739	public Conversation find(final Iterable<Conversation> haystack, final Account account, final Jid jid) {
1740		if (jid == null) {
1741			return null;
1742		}
1743		for (final Conversation conversation : haystack) {
1744			if ((account == null || conversation.getAccount() == account)
1745					&& (conversation.getJid().asBareJid().equals(jid.asBareJid()))) {
1746				return conversation;
1747			}
1748		}
1749		return null;
1750	}
1751
1752	public boolean isConversationsListEmpty(final Conversation ignore) {
1753		synchronized (this.conversations) {
1754			final int size = this.conversations.size();
1755			return size == 0 || size == 1 && this.conversations.get(0) == ignore;
1756		}
1757	}
1758
1759	public boolean isConversationStillOpen(final Conversation conversation) {
1760		synchronized (this.conversations) {
1761			for (Conversation current : this.conversations) {
1762				if (current == conversation) {
1763					return true;
1764				}
1765			}
1766		}
1767		return false;
1768	}
1769
1770	public Conversation findOrCreateConversation(Account account, Jid jid, boolean muc, final boolean async) {
1771		return this.findOrCreateConversation(account, jid, muc, false, async);
1772	}
1773
1774	public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final boolean async) {
1775		return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, null, async);
1776	}
1777
1778	public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final MessageArchiveService.Query query, final boolean async) {
1779		synchronized (this.conversations) {
1780			Conversation conversation = find(account, jid);
1781			if (conversation != null) {
1782				return conversation;
1783			}
1784			conversation = databaseBackend.findConversation(account, jid);
1785			final boolean loadMessagesFromDb;
1786			if (conversation != null) {
1787				conversation.setStatus(Conversation.STATUS_AVAILABLE);
1788				conversation.setAccount(account);
1789				if (muc) {
1790					conversation.setMode(Conversation.MODE_MULTI);
1791					conversation.setContactJid(jid);
1792				} else {
1793					conversation.setMode(Conversation.MODE_SINGLE);
1794					conversation.setContactJid(jid.asBareJid());
1795				}
1796				databaseBackend.updateConversation(conversation);
1797				loadMessagesFromDb = conversation.messagesLoaded.compareAndSet(true, false);
1798			} else {
1799				String conversationName;
1800				Contact contact = account.getRoster().getContact(jid);
1801				if (contact != null) {
1802					conversationName = contact.getDisplayName();
1803				} else {
1804					conversationName = jid.getLocal();
1805				}
1806				if (muc) {
1807					conversation = new Conversation(conversationName, account, jid,
1808							Conversation.MODE_MULTI);
1809				} else {
1810					conversation = new Conversation(conversationName, account, jid.asBareJid(),
1811							Conversation.MODE_SINGLE);
1812				}
1813				this.databaseBackend.createConversation(conversation);
1814				loadMessagesFromDb = false;
1815			}
1816			final Conversation c = conversation;
1817			final Runnable runnable = () -> {
1818				if (loadMessagesFromDb) {
1819					c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE));
1820					updateConversationUi();
1821					c.messagesLoaded.set(true);
1822				}
1823				if (account.getXmppConnection() != null
1824						&& !c.getContact().isBlocked()
1825						&& account.getXmppConnection().getFeatures().mam()
1826						&& !muc) {
1827					if (query == null) {
1828						mMessageArchiveService.query(c);
1829					} else {
1830						if (query.getConversation() == null) {
1831							mMessageArchiveService.query(c, query.getStart(), query.isCatchup());
1832						}
1833					}
1834				}
1835				if (joinAfterCreate) {
1836					joinMuc(c);
1837				}
1838			};
1839			if (async) {
1840				mDatabaseReaderExecutor.execute(runnable);
1841			} else {
1842				runnable.run();
1843			}
1844			this.conversations.add(conversation);
1845			updateConversationUi();
1846			return conversation;
1847		}
1848	}
1849
1850	public void archiveConversation(Conversation conversation) {
1851	    archiveConversation(conversation, true);
1852    }
1853
1854	private void archiveConversation(Conversation conversation, final boolean maySyncronizeWithBookmarks) {
1855		getNotificationService().clear(conversation);
1856		conversation.setStatus(Conversation.STATUS_ARCHIVED);
1857		conversation.setNextMessage(null);
1858		synchronized (this.conversations) {
1859			getMessageArchiveService().kill(conversation);
1860			if (conversation.getMode() == Conversation.MODE_MULTI) {
1861				if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1862					Bookmark bookmark = conversation.getBookmark();
1863					if (maySyncronizeWithBookmarks && bookmark != null && synchronizeWithBookmarks()) {
1864						if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) {
1865							Account account = bookmark.getAccount();
1866							bookmark.setConversation(null);
1867							account.getBookmarks().remove(bookmark);
1868							pushBookmarks(account);
1869						} else if (bookmark.autojoin()) {
1870							bookmark.setAutojoin(false);
1871							pushBookmarks(bookmark.getAccount());
1872						}
1873					}
1874				}
1875				leaveMuc(conversation);
1876			} else {
1877				if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1878				    stopPresenceUpdatesTo(conversation.getContact());
1879				}
1880			}
1881			updateConversation(conversation);
1882			this.conversations.remove(conversation);
1883			updateConversationUi();
1884		}
1885	}
1886
1887	public void stopPresenceUpdatesTo(Contact contact) {
1888        Log.d(Config.LOGTAG, "Canceling presence request from " + contact.getJid().toString());
1889        sendPresencePacket(contact.getAccount(), mPresenceGenerator.stopPresenceUpdatesTo(contact));
1890        contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
1891    }
1892
1893	public void createAccount(final Account account) {
1894		account.initAccountServices(this);
1895		databaseBackend.createAccount(account);
1896		this.accounts.add(account);
1897		this.reconnectAccountInBackground(account);
1898		updateAccountUi();
1899		syncEnabledAccountSetting();
1900		toggleForegroundService();
1901	}
1902
1903	private void syncEnabledAccountSetting() {
1904		getPreferences().edit().putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts()).apply();
1905	}
1906
1907	public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
1908		new Thread(() -> {
1909			try {
1910				final X509Certificate[] chain = KeyChain.getCertificateChain(this, alias);
1911				final X509Certificate cert = chain != null && chain.length > 0 ? chain[0] : null;
1912				if (cert == null) {
1913					callback.informUser(R.string.unable_to_parse_certificate);
1914					return;
1915				}
1916				Pair<Jid, String> info = CryptoHelper.extractJidAndName(cert);
1917				if (info == null) {
1918					callback.informUser(R.string.certificate_does_not_contain_jid);
1919					return;
1920				}
1921				if (findAccountByJid(info.first) == null) {
1922					Account account = new Account(info.first, "");
1923					account.setPrivateKeyAlias(alias);
1924					account.setOption(Account.OPTION_DISABLED, true);
1925					account.setDisplayName(info.second);
1926					createAccount(account);
1927					callback.onAccountCreated(account);
1928					if (Config.X509_VERIFICATION) {
1929						try {
1930							getMemorizingTrustManager().getNonInteractive(account.getJid().getDomain()).checkClientTrusted(chain, "RSA");
1931						} catch (CertificateException e) {
1932							callback.informUser(R.string.certificate_chain_is_not_trusted);
1933						}
1934					}
1935				} else {
1936					callback.informUser(R.string.account_already_exists);
1937				}
1938			} catch (Exception e) {
1939				e.printStackTrace();
1940				callback.informUser(R.string.unable_to_parse_certificate);
1941			}
1942		}).start();
1943
1944	}
1945
1946	public void updateKeyInAccount(final Account account, final String alias) {
1947		Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": update key in account " + alias);
1948		try {
1949			X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
1950			Log.d(Config.LOGTAG, account.getJid().asBareJid() + " loaded certificate chain");
1951			Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
1952			if (info == null) {
1953				showErrorToastInUi(R.string.certificate_does_not_contain_jid);
1954				return;
1955			}
1956			if (account.getJid().asBareJid().equals(info.first)) {
1957				account.setPrivateKeyAlias(alias);
1958				account.setDisplayName(info.second);
1959				databaseBackend.updateAccount(account);
1960				if (Config.X509_VERIFICATION) {
1961					try {
1962						getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
1963					} catch (CertificateException e) {
1964						showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
1965					}
1966					account.getAxolotlService().regenerateKeys(true);
1967				}
1968			} else {
1969				showErrorToastInUi(R.string.jid_does_not_match_certificate);
1970			}
1971		} catch (Exception e) {
1972			e.printStackTrace();
1973		}
1974	}
1975
1976	public boolean updateAccount(final Account account) {
1977		if (databaseBackend.updateAccount(account)) {
1978			account.setShowErrorNotification(true);
1979			this.statusListener.onStatusChanged(account);
1980			databaseBackend.updateAccount(account);
1981			reconnectAccountInBackground(account);
1982			updateAccountUi();
1983			getNotificationService().updateErrorNotification();
1984			toggleForegroundService();
1985			syncEnabledAccountSetting();
1986			return true;
1987		} else {
1988			return false;
1989		}
1990	}
1991
1992	public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) {
1993		final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword);
1994		sendIqPacket(account, iq, (a, packet) -> {
1995			if (packet.getType() == IqPacket.TYPE.RESULT) {
1996				a.setPassword(newPassword);
1997				a.setOption(Account.OPTION_MAGIC_CREATE, false);
1998				databaseBackend.updateAccount(a);
1999				callback.onPasswordChangeSucceeded();
2000			} else {
2001				callback.onPasswordChangeFailed();
2002			}
2003		});
2004	}
2005
2006	public void deleteAccount(final Account account) {
2007		synchronized (this.conversations) {
2008			for (final Conversation conversation : conversations) {
2009				if (conversation.getAccount() == account) {
2010					if (conversation.getMode() == Conversation.MODE_MULTI) {
2011						leaveMuc(conversation);
2012					}
2013					conversations.remove(conversation);
2014				}
2015			}
2016			if (account.getXmppConnection() != null) {
2017				new Thread(() -> disconnect(account, true)).start();
2018			}
2019			final Runnable runnable = () -> {
2020				if (!databaseBackend.deleteAccount(account)) {
2021					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to delete account");
2022				}
2023			};
2024			mDatabaseWriterExecutor.execute(runnable);
2025			this.accounts.remove(account);
2026			this.mRosterSyncTaskManager.clear(account);
2027			updateAccountUi();
2028			getNotificationService().updateErrorNotification();
2029			syncEnabledAccountSetting();
2030			toggleForegroundService();
2031		}
2032	}
2033
2034	public void setOnConversationListChangedListener(OnConversationUpdate listener) {
2035		final boolean remainingListeners;
2036		synchronized (LISTENER_LOCK) {
2037			remainingListeners = checkListeners();
2038			if (!this.mOnConversationUpdates.add(listener)) {
2039				Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as ConversationListChangedListener");
2040			}
2041			this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
2042		}
2043		if (remainingListeners) {
2044			switchToForeground();
2045		}
2046	}
2047
2048	public void removeOnConversationListChangedListener(OnConversationUpdate listener) {
2049		final boolean remainingListeners;
2050		synchronized (LISTENER_LOCK) {
2051			this.mOnConversationUpdates.remove(listener);
2052			this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
2053			remainingListeners = checkListeners();
2054		}
2055		if (remainingListeners) {
2056			switchToBackground();
2057		}
2058	}
2059
2060	public void setOnShowErrorToastListener(OnShowErrorToast listener) {
2061		final boolean remainingListeners;
2062		synchronized (LISTENER_LOCK) {
2063			remainingListeners = checkListeners();
2064			if (!this.mOnShowErrorToasts.add(listener)) {
2065				Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnShowErrorToastListener");
2066			}
2067		}
2068		if (remainingListeners) {
2069			switchToForeground();
2070		}
2071	}
2072
2073	public void removeOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
2074		final boolean remainingListeners;
2075		synchronized (LISTENER_LOCK) {
2076			this.mOnShowErrorToasts.remove(onShowErrorToast);
2077			remainingListeners = checkListeners();
2078		}
2079		if (remainingListeners) {
2080			switchToBackground();
2081		}
2082	}
2083
2084	public void setOnAccountListChangedListener(OnAccountUpdate listener) {
2085		final boolean remainingListeners;
2086		synchronized (LISTENER_LOCK) {
2087			remainingListeners = checkListeners();
2088			if (!this.mOnAccountUpdates.add(listener)) {
2089				Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnAccountListChangedtListener");
2090			}
2091		}
2092		if (remainingListeners) {
2093			switchToForeground();
2094		}
2095	}
2096
2097	public void removeOnAccountListChangedListener(OnAccountUpdate listener) {
2098		final boolean remainingListeners;
2099		synchronized (LISTENER_LOCK) {
2100			this.mOnAccountUpdates.remove(listener);
2101			remainingListeners = checkListeners();
2102		}
2103		if (remainingListeners) {
2104			switchToBackground();
2105		}
2106	}
2107
2108	public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
2109		final boolean remainingListeners;
2110		synchronized (LISTENER_LOCK) {
2111			remainingListeners = checkListeners();
2112			if (!this.mOnCaptchaRequested.add(listener)) {
2113				Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnCaptchaRequestListener");
2114			}
2115		}
2116		if (remainingListeners) {
2117			switchToForeground();
2118		}
2119	}
2120
2121	public void removeOnCaptchaRequestedListener(OnCaptchaRequested listener) {
2122		final boolean remainingListeners;
2123		synchronized (LISTENER_LOCK) {
2124			this.mOnCaptchaRequested.remove(listener);
2125			remainingListeners = checkListeners();
2126		}
2127		if (remainingListeners) {
2128			switchToBackground();
2129		}
2130	}
2131
2132	public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
2133		final boolean remainingListeners;
2134		synchronized (LISTENER_LOCK) {
2135			remainingListeners = checkListeners();
2136			if (!this.mOnRosterUpdates.add(listener)) {
2137				Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnRosterUpdateListener");
2138			}
2139		}
2140		if (remainingListeners) {
2141			switchToForeground();
2142		}
2143	}
2144
2145	public void removeOnRosterUpdateListener(final OnRosterUpdate listener) {
2146		final boolean remainingListeners;
2147		synchronized (LISTENER_LOCK) {
2148			this.mOnRosterUpdates.remove(listener);
2149			remainingListeners = checkListeners();
2150		}
2151		if (remainingListeners) {
2152			switchToBackground();
2153		}
2154	}
2155
2156	public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
2157		final boolean remainingListeners;
2158		synchronized (LISTENER_LOCK) {
2159			remainingListeners = checkListeners();
2160			if (!this.mOnUpdateBlocklist.add(listener)) {
2161				Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnUpdateBlocklistListener");
2162			}
2163		}
2164		if (remainingListeners) {
2165			switchToForeground();
2166		}
2167	}
2168
2169	public void removeOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
2170		final boolean remainingListeners;
2171		synchronized (LISTENER_LOCK) {
2172			this.mOnUpdateBlocklist.remove(listener);
2173			remainingListeners = checkListeners();
2174		}
2175		if (remainingListeners) {
2176			switchToBackground();
2177		}
2178	}
2179
2180	public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
2181		final boolean remainingListeners;
2182		synchronized (LISTENER_LOCK) {
2183			remainingListeners = checkListeners();
2184			if (!this.mOnKeyStatusUpdated.add(listener)) {
2185				Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnKeyStatusUpdateListener");
2186			}
2187		}
2188		if (remainingListeners) {
2189			switchToForeground();
2190		}
2191	}
2192
2193	public void removeOnNewKeysAvailableListener(final OnKeyStatusUpdated listener) {
2194		final boolean remainingListeners;
2195		synchronized (LISTENER_LOCK) {
2196			this.mOnKeyStatusUpdated.remove(listener);
2197			remainingListeners = checkListeners();
2198		}
2199		if (remainingListeners) {
2200			switchToBackground();
2201		}
2202	}
2203
2204	public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
2205		final boolean remainingListeners;
2206		synchronized (LISTENER_LOCK) {
2207			remainingListeners = checkListeners();
2208			if (!this.mOnMucRosterUpdate.add(listener)) {
2209				Log.w(Config.LOGTAG,listener.getClass().getName()+" is already registered as OnMucRosterListener");
2210			}
2211		}
2212		if (remainingListeners) {
2213			switchToForeground();
2214		}
2215	}
2216
2217	public void removeOnMucRosterUpdateListener(final OnMucRosterUpdate listener) {
2218		final boolean remainingListeners;
2219		synchronized (LISTENER_LOCK) {
2220			this.mOnMucRosterUpdate.remove(listener);
2221			remainingListeners = checkListeners();
2222		}
2223		if (remainingListeners) {
2224			switchToBackground();
2225		}
2226	}
2227
2228	public boolean checkListeners() {
2229		return (this.mOnAccountUpdates.size() == 0
2230				&& this.mOnConversationUpdates.size() == 0
2231				&& this.mOnRosterUpdates.size() == 0
2232				&& this.mOnCaptchaRequested.size() == 0
2233				&& this.mOnMucRosterUpdate.size() == 0
2234				&& this.mOnUpdateBlocklist.size() == 0
2235				&& this.mOnShowErrorToasts.size() == 0
2236				&& this.mOnKeyStatusUpdated.size() == 0);
2237	}
2238
2239	private void switchToForeground() {
2240		final boolean broadcastLastActivity = broadcastLastActivity();
2241		for (Conversation conversation : getConversations()) {
2242			if (conversation.getMode() == Conversation.MODE_MULTI) {
2243				conversation.getMucOptions().resetChatState();
2244			} else {
2245				conversation.setIncomingChatState(Config.DEFAULT_CHATSTATE);
2246			}
2247		}
2248		for (Account account : getAccounts()) {
2249			if (account.getStatus() == Account.State.ONLINE) {
2250				account.deactivateGracePeriod();
2251				final XmppConnection connection = account.getXmppConnection();
2252				if (connection != null) {
2253					if (connection.getFeatures().csi()) {
2254						connection.sendActive();
2255					}
2256					if (broadcastLastActivity) {
2257						sendPresence(account, false); //send new presence but don't include idle because we are not
2258					}
2259				}
2260			}
2261		}
2262		Log.d(Config.LOGTAG, "app switched into foreground");
2263	}
2264
2265	private void switchToBackground() {
2266		final boolean broadcastLastActivity = broadcastLastActivity();
2267		if (broadcastLastActivity) {
2268			mLastActivity = System.currentTimeMillis();
2269			final SharedPreferences.Editor editor = getPreferences().edit();
2270			editor.putLong(SETTING_LAST_ACTIVITY_TS, mLastActivity);
2271			editor.apply();
2272		}
2273		for (Account account : getAccounts()) {
2274			if (account.getStatus() == Account.State.ONLINE) {
2275				XmppConnection connection = account.getXmppConnection();
2276				if (connection != null) {
2277					if (broadcastLastActivity) {
2278						sendPresence(account, true);
2279					}
2280					if (connection.getFeatures().csi()) {
2281						connection.sendInactive();
2282					}
2283				}
2284			}
2285		}
2286		this.mNotificationService.setIsInForeground(false);
2287		Log.d(Config.LOGTAG, "app switched into background");
2288	}
2289
2290	private void connectMultiModeConversations(Account account) {
2291		List<Conversation> conversations = getConversations();
2292		for (Conversation conversation : conversations) {
2293			if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) {
2294				joinMuc(conversation);
2295			}
2296		}
2297	}
2298
2299	public void joinMuc(Conversation conversation) {
2300		joinMuc(conversation, null, false);
2301	}
2302
2303	public void joinMuc(Conversation conversation, boolean followedInvite) {
2304		joinMuc(conversation, null, followedInvite);
2305	}
2306
2307	private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
2308		joinMuc(conversation, onConferenceJoined, false);
2309	}
2310
2311	private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined, final boolean followedInvite) {
2312		Account account = conversation.getAccount();
2313		account.pendingConferenceJoins.remove(conversation);
2314		account.pendingConferenceLeaves.remove(conversation);
2315		if (account.getStatus() == Account.State.ONLINE) {
2316			sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions()));
2317			conversation.resetMucOptions();
2318			if (onConferenceJoined != null) {
2319				conversation.getMucOptions().flagNoAutoPushConfiguration();
2320			}
2321			conversation.setHasMessagesLeftOnServer(false);
2322			fetchConferenceConfiguration(conversation, new OnConferenceConfigurationFetched() {
2323
2324				private void join(Conversation conversation) {
2325					Account account = conversation.getAccount();
2326					final MucOptions mucOptions = conversation.getMucOptions();
2327
2328					if (mucOptions.nonanonymous() && !mucOptions.membersOnly() && !conversation.getBooleanAttribute("accept_non_anonymous", false)) {
2329					    mucOptions.setError(MucOptions.Error.NON_ANONYMOUS);
2330					    updateConversationUi();
2331					    return;
2332                    }
2333
2334					final Jid joinJid = mucOptions.getSelf().getFullJid();
2335					Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": joining conversation " + joinJid.toString());
2336					PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null);
2337					packet.setTo(joinJid);
2338					Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
2339					if (conversation.getMucOptions().getPassword() != null) {
2340						x.addChild("password").setContent(mucOptions.getPassword());
2341					}
2342
2343					if (mucOptions.mamSupport()) {
2344						// Use MAM instead of the limited muc history to get history
2345						x.addChild("history").setAttribute("maxchars", "0");
2346					} else {
2347						// Fallback to muc history
2348						x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted().getTimestamp()));
2349					}
2350					sendPresencePacket(account, packet);
2351					if (onConferenceJoined != null) {
2352						onConferenceJoined.onConferenceJoined(conversation);
2353					}
2354					if (!joinJid.equals(conversation.getJid())) {
2355						conversation.setContactJid(joinJid);
2356						databaseBackend.updateConversation(conversation);
2357					}
2358
2359					if (mucOptions.mamSupport()) {
2360						getMessageArchiveService().catchupMUC(conversation);
2361					}
2362					if (mucOptions.isPrivateAndNonAnonymous()) {
2363						fetchConferenceMembers(conversation);
2364						if (followedInvite && conversation.getBookmark() == null) {
2365							saveConversationAsBookmark(conversation, null);
2366						}
2367					}
2368					sendUnsentMessages(conversation);
2369				}
2370
2371				@Override
2372				public void onConferenceConfigurationFetched(Conversation conversation) {
2373				    if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
2374				        Log.d(Config.LOGTAG,account.getJid().asBareJid()+": conversation ("+conversation.getJid()+") got archived before IQ result");
2375				        return;
2376                    }
2377					join(conversation);
2378				}
2379
2380				@Override
2381				public void onFetchFailed(final Conversation conversation, Element error) {
2382                    if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
2383                        Log.d(Config.LOGTAG,account.getJid().asBareJid()+": conversation ("+conversation.getJid()+") got archived before IQ result");
2384                        return;
2385                    }
2386					if (error != null && "remote-server-not-found".equals(error.getName())) {
2387						conversation.getMucOptions().setError(MucOptions.Error.SERVER_NOT_FOUND);
2388						updateConversationUi();
2389					} else {
2390						join(conversation);
2391						fetchConferenceConfiguration(conversation);
2392					}
2393				}
2394			});
2395			updateConversationUi();
2396		} else {
2397			account.pendingConferenceJoins.add(conversation);
2398			conversation.resetMucOptions();
2399			conversation.setHasMessagesLeftOnServer(false);
2400			updateConversationUi();
2401		}
2402	}
2403
2404	private void fetchConferenceMembers(final Conversation conversation) {
2405		final Account account = conversation.getAccount();
2406		final AxolotlService axolotlService = account.getAxolotlService();
2407		final String[] affiliations = {"member", "admin", "owner"};
2408		OnIqPacketReceived callback = new OnIqPacketReceived() {
2409
2410			private int i = 0;
2411			private boolean success = true;
2412
2413			@Override
2414			public void onIqPacketReceived(Account account, IqPacket packet) {
2415				final boolean omemoEnabled = conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL;
2416				Element query = packet.query("http://jabber.org/protocol/muc#admin");
2417				if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
2418					for (Element child : query.getChildren()) {
2419						if ("item".equals(child.getName())) {
2420							MucOptions.User user = AbstractParser.parseItem(conversation, child);
2421							if (!user.realJidMatchesAccount()) {
2422								boolean isNew = conversation.getMucOptions().updateUser(user);
2423								Contact contact = user.getContact();
2424								if (omemoEnabled
2425										&& isNew
2426										&& user.getRealJid() != null
2427										&& (contact == null || !contact.mutualPresenceSubscription())
2428										&& axolotlService.hasEmptyDeviceList(user.getRealJid())) {
2429									axolotlService.fetchDeviceIds(user.getRealJid());
2430								}
2431							}
2432						}
2433					}
2434				} else {
2435					success = false;
2436					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not request affiliation " + affiliations[i] + " in " + conversation.getJid().asBareJid());
2437				}
2438				++i;
2439				if (i >= affiliations.length) {
2440					List<Jid> members = conversation.getMucOptions().getMembers(true);
2441					if (success) {
2442						List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
2443						boolean changed = false;
2444						for (ListIterator<Jid> iterator = cryptoTargets.listIterator(); iterator.hasNext(); ) {
2445							Jid jid = iterator.next();
2446							if (!members.contains(jid) && !members.contains(Jid.ofDomain(jid.getDomain()))) {
2447								iterator.remove();
2448								Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName());
2449								changed = true;
2450							}
2451						}
2452						if (changed) {
2453							conversation.setAcceptedCryptoTargets(cryptoTargets);
2454							updateConversation(conversation);
2455						}
2456					}
2457					getAvatarService().clear(conversation);
2458					updateMucRosterUi();
2459					updateConversationUi();
2460				}
2461			}
2462		};
2463		for (String affiliation : affiliations) {
2464			sendIqPacket(account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
2465		}
2466		Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching members for " + conversation.getName());
2467	}
2468
2469	public void providePasswordForMuc(Conversation conversation, String password) {
2470		if (conversation.getMode() == Conversation.MODE_MULTI) {
2471			conversation.getMucOptions().setPassword(password);
2472			if (conversation.getBookmark() != null) {
2473				if (synchronizeWithBookmarks()) {
2474					conversation.getBookmark().setAutojoin(true);
2475				}
2476				pushBookmarks(conversation.getAccount());
2477			}
2478			updateConversation(conversation);
2479			joinMuc(conversation);
2480		}
2481	}
2482
2483	private boolean hasEnabledAccounts() {
2484		for (Account account : this.accounts) {
2485			if (account.isEnabled()) {
2486				return true;
2487			}
2488		}
2489		return false;
2490	}
2491
2492
2493	public void getAttachments(final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) {
2494        getAttachments(conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded);
2495    }
2496
2497    public void getAttachments(final Account account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) {
2498        getAttachments(account.getUuid(),jid.asBareJid(),limit, onMediaLoaded);
2499    }
2500
2501
2502	public void getAttachments(final String account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) {
2503        new Thread(() -> onMediaLoaded.onMediaLoaded(fileBackend.convertToAttachments(databaseBackend.getRelativeFilePaths(account, jid, limit)))).start();
2504    }
2505
2506	public void persistSelfNick(MucOptions.User self) {
2507		final Conversation conversation = self.getConversation();
2508		final boolean tookProposedNickFromBookmark = conversation.getMucOptions().isTookProposedNickFromBookmark();
2509		Jid full = self.getFullJid();
2510		if (!full.equals(conversation.getJid())) {
2511			Log.d(Config.LOGTAG, "nick changed. updating");
2512			conversation.setContactJid(full);
2513			databaseBackend.updateConversation(conversation);
2514		}
2515
2516		final Bookmark bookmark = conversation.getBookmark();
2517		final String bookmarkedNick = bookmark == null ? null : bookmark.getNick();
2518        if (bookmark != null && (tookProposedNickFromBookmark || TextUtils.isEmpty(bookmarkedNick)) && !full.getResource().equals(bookmarkedNick)) {
2519            Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": persist nick '" + full.getResource() + "' into bookmark for " + conversation.getJid().asBareJid());
2520            bookmark.setNick(full.getResource());
2521            pushBookmarks(bookmark.getAccount());
2522        }
2523	}
2524
2525	public boolean renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
2526		final MucOptions options = conversation.getMucOptions();
2527		final Jid joinJid = options.createJoinJid(nick);
2528		if (joinJid == null) {
2529			return false;
2530		}
2531		if (options.online()) {
2532			Account account = conversation.getAccount();
2533			options.setOnRenameListener(new OnRenameListener() {
2534
2535				@Override
2536				public void onSuccess() {
2537					callback.success(conversation);
2538				}
2539
2540				@Override
2541				public void onFailure() {
2542					callback.error(R.string.nick_in_use, conversation);
2543				}
2544			});
2545
2546			PresencePacket packet = new PresencePacket();
2547			packet.setTo(joinJid);
2548			packet.setFrom(conversation.getAccount().getJid());
2549
2550			String sig = account.getPgpSignature();
2551			if (sig != null) {
2552				packet.addChild("status").setContent("online");
2553				packet.addChild("x", "jabber:x:signed").setContent(sig);
2554			}
2555			sendPresencePacket(account, packet);
2556		} else {
2557			conversation.setContactJid(joinJid);
2558			databaseBackend.updateConversation(conversation);
2559			if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
2560				Bookmark bookmark = conversation.getBookmark();
2561				if (bookmark != null) {
2562					bookmark.setNick(nick);
2563					pushBookmarks(bookmark.getAccount());
2564				}
2565				joinMuc(conversation);
2566			}
2567		}
2568		return true;
2569	}
2570
2571	public void leaveMuc(Conversation conversation) {
2572		leaveMuc(conversation, false);
2573	}
2574
2575	private void leaveMuc(Conversation conversation, boolean now) {
2576		Account account = conversation.getAccount();
2577		account.pendingConferenceJoins.remove(conversation);
2578		account.pendingConferenceLeaves.remove(conversation);
2579		if (account.getStatus() == Account.State.ONLINE || now) {
2580			sendPresencePacket(conversation.getAccount(), mPresenceGenerator.leave(conversation.getMucOptions()));
2581			conversation.getMucOptions().setOffline();
2582			Bookmark bookmark = conversation.getBookmark();
2583			if (bookmark != null) {
2584				bookmark.setConversation(null);
2585			}
2586			Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": leaving muc " + conversation.getJid());
2587		} else {
2588			account.pendingConferenceLeaves.add(conversation);
2589		}
2590	}
2591
2592	public String findConferenceServer(final Account account) {
2593		String server;
2594		if (account.getXmppConnection() != null) {
2595			server = account.getXmppConnection().getMucServer();
2596			if (server != null) {
2597				return server;
2598			}
2599		}
2600		for (Account other : getAccounts()) {
2601			if (other != account && other.getXmppConnection() != null) {
2602				server = other.getXmppConnection().getMucServer();
2603				if (server != null) {
2604					return server;
2605				}
2606			}
2607		}
2608		return null;
2609	}
2610
2611	public boolean createAdhocConference(final Account account,
2612	                                     final String name,
2613	                                     final Iterable<Jid> jids,
2614	                                     final UiCallback<Conversation> callback) {
2615		Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": creating adhoc conference with " + jids.toString());
2616		if (account.getStatus() == Account.State.ONLINE) {
2617			try {
2618				String server = findConferenceServer(account);
2619				if (server == null) {
2620					if (callback != null) {
2621						callback.error(R.string.no_conference_server_found, null);
2622					}
2623					return false;
2624				}
2625				final Jid jid = Jid.of(CryptoHelper.pronounceable(getRNG()), server, null);
2626				final Conversation conversation = findOrCreateConversation(account, jid, true, false, true);
2627				joinMuc(conversation, new OnConferenceJoined() {
2628					@Override
2629					public void onConferenceJoined(final Conversation conversation) {
2630						final Bundle configuration = IqGenerator.defaultRoomConfiguration();
2631						if (!TextUtils.isEmpty(name)) {
2632							configuration.putString("muc#roomconfig_roomname", name);
2633						}
2634						pushConferenceConfiguration(conversation, configuration, new OnConfigurationPushed() {
2635							@Override
2636							public void onPushSucceeded() {
2637								for (Jid invite : jids) {
2638									invite(conversation, invite);
2639								}
2640								if (account.countPresences() > 1) {
2641									directInvite(conversation, account.getJid().asBareJid());
2642								}
2643								saveConversationAsBookmark(conversation, name);
2644								if (callback != null) {
2645									callback.success(conversation);
2646								}
2647							}
2648
2649							@Override
2650							public void onPushFailed() {
2651								archiveConversation(conversation);
2652								if (callback != null) {
2653									callback.error(R.string.conference_creation_failed, conversation);
2654								}
2655							}
2656						});
2657					}
2658				});
2659				return true;
2660			} catch (IllegalArgumentException e) {
2661				if (callback != null) {
2662					callback.error(R.string.conference_creation_failed, null);
2663				}
2664				return false;
2665			}
2666		} else {
2667			if (callback != null) {
2668				callback.error(R.string.not_connected_try_again, null);
2669			}
2670			return false;
2671		}
2672	}
2673
2674	public void fetchConferenceConfiguration(final Conversation conversation) {
2675		fetchConferenceConfiguration(conversation, null);
2676	}
2677
2678	public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) {
2679		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2680		request.setTo(conversation.getJid().asBareJid());
2681		request.query("http://jabber.org/protocol/disco#info");
2682		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
2683			@Override
2684			public void onIqPacketReceived(Account account, IqPacket packet) {
2685				if (packet.getType() == IqPacket.TYPE.RESULT) {
2686
2687					final MucOptions mucOptions = conversation.getMucOptions();
2688					final Bookmark bookmark = conversation.getBookmark();
2689					final boolean sameBefore = StringUtils.equals(bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName());
2690
2691					if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(packet))) {
2692						Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid());
2693						updateConversation(conversation);
2694					}
2695
2696					if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) {
2697						if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) {
2698							pushBookmarks(account);
2699						}
2700					}
2701
2702
2703					if (callback != null) {
2704						callback.onConferenceConfigurationFetched(conversation);
2705					}
2706
2707
2708
2709					updateConversationUi();
2710				} else if (packet.getType() == IqPacket.TYPE.ERROR) {
2711					if (callback != null) {
2712						callback.onFetchFailed(conversation, packet.getError());
2713					}
2714				}
2715			}
2716		});
2717	}
2718
2719	public void pushNodeConfiguration(Account account, final String node, final Bundle options, final OnConfigurationPushed callback) {
2720		pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback);
2721	}
2722
2723	public void pushNodeConfiguration(Account account, final Jid jid, final String node, final Bundle options, final OnConfigurationPushed callback) {
2724        Log.d(Config.LOGTAG,"pushing node configuration");
2725		sendIqPacket(account, mIqGenerator.requestPubsubConfiguration(jid, node), new OnIqPacketReceived() {
2726			@Override
2727			public void onIqPacketReceived(Account account, IqPacket packet) {
2728				if (packet.getType() == IqPacket.TYPE.RESULT) {
2729					Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub#owner");
2730					Element configuration = pubsub == null ? null : pubsub.findChild("configure");
2731					Element x = configuration == null ? null : configuration.findChild("x", Namespace.DATA);
2732					if (x != null) {
2733						Data data = Data.parse(x);
2734						data.submit(options);
2735						sendIqPacket(account, mIqGenerator.publishPubsubConfiguration(jid, node, data), new OnIqPacketReceived() {
2736							@Override
2737							public void onIqPacketReceived(Account account, IqPacket packet) {
2738								if (packet.getType() == IqPacket.TYPE.RESULT && callback != null) {
2739									Log.d(Config.LOGTAG,account.getJid().asBareJid()+": successfully changed node configuration for node "+node);
2740									callback.onPushSucceeded();
2741								} else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) {
2742									callback.onPushFailed();
2743								}
2744							}
2745						});
2746					} else if (callback != null) {
2747						callback.onPushFailed();
2748					}
2749				} else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) {
2750					callback.onPushFailed();
2751				}
2752			}
2753		});
2754	}
2755
2756	public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConfigurationPushed callback) {
2757		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2758		request.setTo(conversation.getJid().asBareJid());
2759		request.query("http://jabber.org/protocol/muc#owner");
2760		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
2761			@Override
2762			public void onIqPacketReceived(Account account, IqPacket packet) {
2763				if (packet.getType() == IqPacket.TYPE.RESULT) {
2764					Data data = Data.parse(packet.query().findChild("x", Namespace.DATA));
2765					data.submit(options);
2766					IqPacket set = new IqPacket(IqPacket.TYPE.SET);
2767					set.setTo(conversation.getJid().asBareJid());
2768					set.query("http://jabber.org/protocol/muc#owner").addChild(data);
2769					sendIqPacket(account, set, new OnIqPacketReceived() {
2770						@Override
2771						public void onIqPacketReceived(Account account, IqPacket packet) {
2772							if (callback != null) {
2773								if (packet.getType() == IqPacket.TYPE.RESULT) {
2774									callback.onPushSucceeded();
2775								} else {
2776									callback.onPushFailed();
2777								}
2778							}
2779						}
2780					});
2781				} else {
2782					if (callback != null) {
2783						callback.onPushFailed();
2784					}
2785				}
2786			}
2787		});
2788	}
2789
2790	public void pushSubjectToConference(final Conversation conference, final String subject) {
2791		MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, StringUtils.nullOnEmpty(subject));
2792		this.sendMessagePacket(conference.getAccount(), packet);
2793	}
2794
2795	public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
2796		final Jid jid = user.asBareJid();
2797		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
2798		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
2799			@Override
2800			public void onIqPacketReceived(Account account, IqPacket packet) {
2801				if (packet.getType() == IqPacket.TYPE.RESULT) {
2802					conference.getMucOptions().changeAffiliation(jid, affiliation);
2803					getAvatarService().clear(conference);
2804					callback.onAffiliationChangedSuccessful(jid);
2805				} else {
2806					callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
2807				}
2808			}
2809		});
2810	}
2811
2812	public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
2813		List<Jid> jids = new ArrayList<>();
2814		for (MucOptions.User user : conference.getMucOptions().getUsers()) {
2815			if (user.getAffiliation() == before && user.getRealJid() != null) {
2816				jids.add(user.getRealJid());
2817			}
2818		}
2819		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
2820		sendIqPacket(conference.getAccount(), request, mDefaultIqHandler);
2821	}
2822
2823	public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) {
2824		IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
2825		Log.d(Config.LOGTAG, request.toString());
2826		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
2827			@Override
2828			public void onIqPacketReceived(Account account, IqPacket packet) {
2829				Log.d(Config.LOGTAG, packet.toString());
2830				if (packet.getType() == IqPacket.TYPE.RESULT) {
2831					callback.onRoleChangedSuccessful(nick);
2832				} else {
2833					callback.onRoleChangeFailed(nick, R.string.could_not_change_role);
2834				}
2835			}
2836		});
2837	}
2838
2839    public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) {
2840        IqPacket request = new IqPacket(IqPacket.TYPE.SET);
2841        request.setTo(conversation.getJid().asBareJid());
2842        request.query("http://jabber.org/protocol/muc#owner").addChild("destroy");
2843        sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
2844            @Override
2845            public void onIqPacketReceived(Account account, IqPacket packet) {
2846                if (packet.getType() == IqPacket.TYPE.RESULT) {
2847                    if (callback != null) {
2848                        callback.onRoomDestroySucceeded();
2849                    }
2850                } else if (packet.getType() == IqPacket.TYPE.ERROR) {
2851                    if (callback != null) {
2852                        callback.onRoomDestroyFailed();
2853                    }
2854                }
2855            }
2856        });
2857    }
2858
2859	private void disconnect(Account account, boolean force) {
2860		if ((account.getStatus() == Account.State.ONLINE)
2861				|| (account.getStatus() == Account.State.DISABLED)) {
2862			final XmppConnection connection = account.getXmppConnection();
2863			if (!force) {
2864				List<Conversation> conversations = getConversations();
2865				for (Conversation conversation : conversations) {
2866					if (conversation.getAccount() == account) {
2867						if (conversation.getMode() == Conversation.MODE_MULTI) {
2868							leaveMuc(conversation, true);
2869						}
2870					}
2871				}
2872				sendOfflinePresence(account);
2873			}
2874			connection.disconnect(force);
2875		}
2876	}
2877
2878	@Override
2879	public IBinder onBind(Intent intent) {
2880		return mBinder;
2881	}
2882
2883	public void updateMessage(Message message) {
2884		updateMessage(message, true);
2885	}
2886
2887	public void updateMessage(Message message, boolean includeBody) {
2888		databaseBackend.updateMessage(message, includeBody);
2889		updateConversationUi();
2890	}
2891
2892	public void updateMessage(Message message, String uuid) {
2893		if (!databaseBackend.updateMessage(message, uuid)) {
2894            Log.e(Config.LOGTAG,"error updated message in DB after edit");
2895        }
2896		updateConversationUi();
2897	}
2898
2899	protected void syncDirtyContacts(Account account) {
2900		for (Contact contact : account.getRoster().getContacts()) {
2901			if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
2902				pushContactToServer(contact);
2903			}
2904			if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
2905				deleteContactOnServer(contact);
2906			}
2907		}
2908	}
2909
2910	public void createContact(Contact contact, boolean autoGrant) {
2911		if (autoGrant) {
2912			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
2913			contact.setOption(Contact.Options.ASKING);
2914		}
2915		pushContactToServer(contact);
2916	}
2917
2918	public void pushContactToServer(final Contact contact) {
2919		contact.resetOption(Contact.Options.DIRTY_DELETE);
2920		contact.setOption(Contact.Options.DIRTY_PUSH);
2921		final Account account = contact.getAccount();
2922		if (account.getStatus() == Account.State.ONLINE) {
2923			final boolean ask = contact.getOption(Contact.Options.ASKING);
2924			final boolean sendUpdates = contact
2925					.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
2926					&& contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
2927			final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2928			iq.query(Namespace.ROSTER).addChild(contact.asElement());
2929			account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
2930			if (sendUpdates) {
2931				sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
2932			}
2933			if (ask) {
2934				sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact));
2935			}
2936		} else {
2937			syncRoster(contact.getAccount());
2938		}
2939	}
2940
2941	public void publishMucAvatar(final Conversation conversation, final Uri image, final OnAvatarPublication callback) {
2942		new Thread(() -> {
2943			final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
2944			final int size = Config.AVATAR_SIZE;
2945			final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
2946			if (avatar != null) {
2947				if (!getFileBackend().save(avatar)) {
2948					callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
2949					return;
2950				}
2951				avatar.owner = conversation.getJid().asBareJid();
2952				publishMucAvatar(conversation, avatar, callback);
2953			} else {
2954				callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
2955			}
2956		}).start();
2957	}
2958
2959	public void publishAvatar(final Account account, final Uri image, final OnAvatarPublication callback) {
2960		new Thread(() -> {
2961			final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
2962			final int size = Config.AVATAR_SIZE;
2963			final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
2964			if (avatar != null) {
2965				if (!getFileBackend().save(avatar)) {
2966					Log.d(Config.LOGTAG,"unable to save vcard");
2967					callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
2968					return;
2969				}
2970				publishAvatar(account, avatar, callback);
2971			} else {
2972				callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
2973			}
2974		}).start();
2975
2976	}
2977
2978	private void publishMucAvatar(Conversation conversation, Avatar avatar, OnAvatarPublication callback) {
2979		final IqPacket retrieve = mIqGenerator.retrieveVcardAvatar(avatar);
2980		sendIqPacket(conversation.getAccount(), retrieve, (account, response) -> {
2981			boolean itemNotFound = response.getType() == IqPacket.TYPE.ERROR && response.hasChild("error") && response.findChild("error").hasChild("item-not-found");
2982			if (response.getType() == IqPacket.TYPE.RESULT || itemNotFound) {
2983				Element vcard = response.findChild("vCard", "vcard-temp");
2984				if (vcard == null) {
2985					vcard = new Element("vCard", "vcard-temp");
2986				}
2987				Element photo = vcard.findChild("PHOTO");
2988				if (photo == null) {
2989					photo = vcard.addChild("PHOTO");
2990				}
2991				photo.clearChildren();
2992				photo.addChild("TYPE").setContent(avatar.type);
2993				photo.addChild("BINVAL").setContent(avatar.image);
2994				IqPacket publication = new IqPacket(IqPacket.TYPE.SET);
2995				publication.setTo(conversation.getJid().asBareJid());
2996				publication.addChild(vcard);
2997				sendIqPacket(account, publication, (a1, publicationResponse) -> {
2998					if (publicationResponse.getType() == IqPacket.TYPE.RESULT) {
2999						callback.onAvatarPublicationSucceeded();
3000					} else {
3001						Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getError());
3002						callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
3003					}
3004				});
3005			} else {
3006				Log.d(Config.LOGTAG, "failed to request vcard " + response.toString());
3007				callback.onAvatarPublicationFailed(R.string.error_publish_avatar_no_server_support);
3008			}
3009		});
3010	}
3011
3012    public void publishAvatar(Account account, final Avatar avatar, final OnAvatarPublication callback) {
3013        final Bundle options;
3014        if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
3015            options = PublishOptions.openAccess();
3016        } else {
3017            options = null;
3018        }
3019        publishAvatar(account, avatar, options, true, callback);
3020    }
3021
3022	public void publishAvatar(Account account, final Avatar avatar, final Bundle options, final boolean retry, final OnAvatarPublication callback) {
3023        Log.d(Config.LOGTAG,account.getJid().asBareJid()+": publishing avatar. options="+options);
3024		IqPacket packet = this.mIqGenerator.publishAvatar(avatar, options);
3025		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
3026
3027			@Override
3028			public void onIqPacketReceived(Account account, IqPacket result) {
3029				if (result.getType() == IqPacket.TYPE.RESULT) {
3030                    publishAvatarMetadata(account, avatar, options,true, callback);
3031                } else if (retry && PublishOptions.preconditionNotMet(result)) {
3032				    pushNodeConfiguration(account, "urn:xmpp:avatar:data", options, new OnConfigurationPushed() {
3033                        @Override
3034                        public void onPushSucceeded() {
3035                            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": changed node configuration for avatar node");
3036                            publishAvatar(account, avatar, options, false, callback);
3037                        }
3038
3039                        @Override
3040                        public void onPushFailed() {
3041                            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to change node configuration for avatar node");
3042                            publishAvatar(account, avatar, null, false, callback);
3043                        }
3044                    });
3045				} else {
3046					Element error = result.findChild("error");
3047					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server rejected avatar " + (avatar.size / 1024) + "KiB " + (error != null ? error.toString() : ""));
3048					if (callback != null) {
3049						callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
3050					}
3051				}
3052			}
3053		});
3054	}
3055
3056	public void publishAvatarMetadata(Account account, final Avatar avatar, final Bundle options, final boolean retry, final OnAvatarPublication callback) {
3057        final IqPacket packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options);
3058        sendIqPacket(account, packet, new OnIqPacketReceived() {
3059            @Override
3060            public void onIqPacketReceived(Account account, IqPacket result) {
3061                if (result.getType() == IqPacket.TYPE.RESULT) {
3062                    if (account.setAvatar(avatar.getFilename())) {
3063                        getAvatarService().clear(account);
3064                        databaseBackend.updateAccount(account);
3065                    }
3066                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": published avatar " + (avatar.size / 1024) + "KiB");
3067                    if (callback != null) {
3068                        callback.onAvatarPublicationSucceeded();
3069                    }
3070                } else if (retry && PublishOptions.preconditionNotMet(result)) {
3071                    pushNodeConfiguration(account, "urn:xmpp:avatar:metadata", options, new OnConfigurationPushed() {
3072                        @Override
3073                        public void onPushSucceeded() {
3074                            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": changed node configuration for avatar meta data node");
3075                            publishAvatarMetadata(account, avatar, options,false, callback);
3076                        }
3077
3078                        @Override
3079                        public void onPushFailed() {
3080                            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to change node configuration for avatar meta data node");
3081                            publishAvatarMetadata(account, avatar,  null,false, callback);
3082                        }
3083                    });
3084                } else {
3085                    if (callback != null) {
3086                        callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
3087                    }
3088                }
3089            }
3090        });
3091    }
3092
3093	public void republishAvatarIfNeeded(Account account) {
3094		if (account.getAxolotlService().isPepBroken()) {
3095			Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping republication of avatar because pep is broken");
3096			return;
3097		}
3098		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
3099		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
3100
3101			private Avatar parseAvatar(IqPacket packet) {
3102				Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
3103				if (pubsub != null) {
3104					Element items = pubsub.findChild("items");
3105					if (items != null) {
3106						return Avatar.parseMetadata(items);
3107					}
3108				}
3109				return null;
3110			}
3111
3112			private boolean errorIsItemNotFound(IqPacket packet) {
3113				Element error = packet.findChild("error");
3114				return packet.getType() == IqPacket.TYPE.ERROR
3115						&& error != null
3116						&& error.hasChild("item-not-found");
3117			}
3118
3119			@Override
3120			public void onIqPacketReceived(Account account, IqPacket packet) {
3121				if (packet.getType() == IqPacket.TYPE.RESULT || errorIsItemNotFound(packet)) {
3122					Avatar serverAvatar = parseAvatar(packet);
3123					if (serverAvatar == null && account.getAvatar() != null) {
3124						Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar());
3125						if (avatar != null) {
3126							Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": avatar on server was null. republishing");
3127							publishAvatar(account, fileBackend.getStoredPepAvatar(account.getAvatar()), null);
3128						} else {
3129							Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": error rereading avatar");
3130						}
3131					}
3132				}
3133			}
3134		});
3135	}
3136
3137	public void fetchAvatar(Account account, Avatar avatar) {
3138		fetchAvatar(account, avatar, null);
3139	}
3140
3141	public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
3142		final String KEY = generateFetchKey(account, avatar);
3143		synchronized (this.mInProgressAvatarFetches) {
3144			if (!this.mInProgressAvatarFetches.contains(KEY)) {
3145				switch (avatar.origin) {
3146					case PEP:
3147						this.mInProgressAvatarFetches.add(KEY);
3148						fetchAvatarPep(account, avatar, callback);
3149						break;
3150					case VCARD:
3151						this.mInProgressAvatarFetches.add(KEY);
3152						fetchAvatarVcard(account, avatar, callback);
3153						break;
3154				}
3155			}
3156		}
3157	}
3158
3159	private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
3160		IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar);
3161		sendIqPacket(account, packet, (a, result) -> {
3162			synchronized (mInProgressAvatarFetches) {
3163				mInProgressAvatarFetches.remove(generateFetchKey(a, avatar));
3164			}
3165			final String ERROR = a.getJid().asBareJid() + ": fetching avatar for " + avatar.owner + " failed ";
3166			if (result.getType() == IqPacket.TYPE.RESULT) {
3167				avatar.image = mIqParser.avatarData(result);
3168				if (avatar.image != null) {
3169					if (getFileBackend().save(avatar)) {
3170						if (a.getJid().asBareJid().equals(avatar.owner)) {
3171							if (a.setAvatar(avatar.getFilename())) {
3172								databaseBackend.updateAccount(a);
3173							}
3174							getAvatarService().clear(a);
3175							updateConversationUi();
3176							updateAccountUi();
3177						} else {
3178							Contact contact = a.getRoster().getContact(avatar.owner);
3179							if (contact.setAvatar(avatar)) {
3180								syncRoster(account);
3181								getAvatarService().clear(contact);
3182								updateConversationUi();
3183								updateRosterUi();
3184							}
3185						}
3186						if (callback != null) {
3187							callback.success(avatar);
3188						}
3189						Log.d(Config.LOGTAG, a.getJid().asBareJid()
3190								+ ": successfully fetched pep avatar for " + avatar.owner);
3191						return;
3192					}
3193				} else {
3194
3195					Log.d(Config.LOGTAG, ERROR + "(parsing error)");
3196				}
3197			} else {
3198				Element error = result.findChild("error");
3199				if (error == null) {
3200					Log.d(Config.LOGTAG, ERROR + "(server error)");
3201				} else {
3202					Log.d(Config.LOGTAG, ERROR + error.toString());
3203				}
3204			}
3205			if (callback != null) {
3206				callback.error(0, null);
3207			}
3208
3209		});
3210	}
3211
3212	private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
3213		IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
3214		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
3215			@Override
3216			public void onIqPacketReceived(Account account, IqPacket packet) {
3217				synchronized (mInProgressAvatarFetches) {
3218					mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
3219				}
3220				if (packet.getType() == IqPacket.TYPE.RESULT) {
3221					Element vCard = packet.findChild("vCard", "vcard-temp");
3222					Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
3223					String image = photo != null ? photo.findChildContent("BINVAL") : null;
3224					if (image != null) {
3225						avatar.image = image;
3226						if (getFileBackend().save(avatar)) {
3227							Log.d(Config.LOGTAG, account.getJid().asBareJid()
3228									+ ": successfully fetched vCard avatar for " + avatar.owner);
3229							if (avatar.owner.isBareJid()) {
3230								if (account.getJid().asBareJid().equals(avatar.owner) && account.getAvatar() == null) {
3231									Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": had no avatar. replacing with vcard");
3232									account.setAvatar(avatar.getFilename());
3233									databaseBackend.updateAccount(account);
3234									getAvatarService().clear(account);
3235									updateAccountUi();
3236								} else {
3237									Contact contact = account.getRoster().getContact(avatar.owner);
3238									if (contact.setAvatar(avatar)) {
3239										syncRoster(account);
3240										getAvatarService().clear(contact);
3241										updateRosterUi();
3242									}
3243								}
3244								updateConversationUi();
3245							} else {
3246								Conversation conversation = find(account, avatar.owner.asBareJid());
3247								if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
3248									MucOptions.User user = conversation.getMucOptions().findUserByFullJid(avatar.owner);
3249									if (user != null) {
3250										if (user.setAvatar(avatar)) {
3251											getAvatarService().clear(user);
3252											updateConversationUi();
3253											updateMucRosterUi();
3254										}
3255										if (user.getRealJid() != null) {
3256										    Contact contact = account.getRoster().getContact(user.getRealJid());
3257										    if (contact.setAvatar(avatar)) {
3258                                                syncRoster(account);
3259                                                getAvatarService().clear(contact);
3260                                                updateRosterUi();
3261                                            }
3262                                        }
3263									}
3264								}
3265							}
3266						}
3267					}
3268				}
3269			}
3270		});
3271	}
3272
3273	public void checkForAvatar(Account account, final UiCallback<Avatar> callback) {
3274		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
3275		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
3276
3277			@Override
3278			public void onIqPacketReceived(Account account, IqPacket packet) {
3279				if (packet.getType() == IqPacket.TYPE.RESULT) {
3280					Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
3281					if (pubsub != null) {
3282						Element items = pubsub.findChild("items");
3283						if (items != null) {
3284							Avatar avatar = Avatar.parseMetadata(items);
3285							if (avatar != null) {
3286								avatar.owner = account.getJid().asBareJid();
3287								if (fileBackend.isAvatarCached(avatar)) {
3288									if (account.setAvatar(avatar.getFilename())) {
3289										databaseBackend.updateAccount(account);
3290									}
3291									getAvatarService().clear(account);
3292									callback.success(avatar);
3293								} else {
3294									fetchAvatarPep(account, avatar, callback);
3295								}
3296								return;
3297							}
3298						}
3299					}
3300				}
3301				callback.error(0, null);
3302			}
3303		});
3304	}
3305
3306	public void deleteContactOnServer(Contact contact) {
3307		contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
3308		contact.resetOption(Contact.Options.DIRTY_PUSH);
3309		contact.setOption(Contact.Options.DIRTY_DELETE);
3310		Account account = contact.getAccount();
3311		if (account.getStatus() == Account.State.ONLINE) {
3312			IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
3313			Element item = iq.query(Namespace.ROSTER).addChild("item");
3314			item.setAttribute("jid", contact.getJid().toString());
3315			item.setAttribute("subscription", "remove");
3316			account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
3317		}
3318	}
3319
3320	public void updateConversation(final Conversation conversation) {
3321		mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation));
3322	}
3323
3324	private void reconnectAccount(final Account account, final boolean force, final boolean interactive) {
3325		synchronized (account) {
3326			XmppConnection connection = account.getXmppConnection();
3327			if (connection == null) {
3328				connection = createConnection(account);
3329				account.setXmppConnection(connection);
3330			}
3331			boolean hasInternet = hasInternetConnection();
3332			if (account.isEnabled() && hasInternet) {
3333				if (!force) {
3334					disconnect(account, false);
3335				}
3336				Thread thread = new Thread(connection);
3337				connection.setInteractive(interactive);
3338				connection.prepareNewConnection();
3339				connection.interrupt();
3340				thread.start();
3341				scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
3342			} else {
3343				disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
3344				account.getRoster().clearPresences();
3345				connection.resetEverything();
3346				final AxolotlService axolotlService = account.getAxolotlService();
3347				if (axolotlService != null) {
3348					axolotlService.resetBrokenness();
3349				}
3350				if (!hasInternet) {
3351					account.setStatus(Account.State.NO_INTERNET);
3352				}
3353			}
3354		}
3355	}
3356
3357	public void reconnectAccountInBackground(final Account account) {
3358		new Thread(() -> reconnectAccount(account, false, true)).start();
3359	}
3360
3361	public void invite(Conversation conversation, Jid contact) {
3362		Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": inviting " + contact + " to " + conversation.getJid().asBareJid());
3363		MessagePacket packet = mMessageGenerator.invite(conversation, contact);
3364		sendMessagePacket(conversation.getAccount(), packet);
3365	}
3366
3367	public void directInvite(Conversation conversation, Jid jid) {
3368		MessagePacket packet = mMessageGenerator.directInvite(conversation, jid);
3369		sendMessagePacket(conversation.getAccount(), packet);
3370	}
3371
3372	public void resetSendingToWaiting(Account account) {
3373		for (Conversation conversation : getConversations()) {
3374			if (conversation.getAccount() == account) {
3375				conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
3376			}
3377		}
3378	}
3379
3380	public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) {
3381		return markMessage(account, recipient, uuid, status, null);
3382	}
3383
3384	public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status, String errorMessage) {
3385		if (uuid == null) {
3386			return null;
3387		}
3388		for (Conversation conversation : getConversations()) {
3389			if (conversation.getJid().asBareJid().equals(recipient) && conversation.getAccount() == account) {
3390				final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
3391				if (message != null) {
3392					markMessage(message, status, errorMessage);
3393				}
3394				return message;
3395			}
3396		}
3397		return null;
3398	}
3399
3400	public boolean markMessage(Conversation conversation, String uuid, int status, String serverMessageId) {
3401		if (uuid == null) {
3402			return false;
3403		} else {
3404			Message message = conversation.findSentMessageWithUuid(uuid);
3405			if (message != null) {
3406				if (message.getServerMsgId() == null) {
3407					message.setServerMsgId(serverMessageId);
3408				}
3409				markMessage(message, status);
3410				return true;
3411			} else {
3412				return false;
3413			}
3414		}
3415	}
3416
3417	public void markMessage(Message message, int status) {
3418		markMessage(message, status, null);
3419	}
3420
3421
3422	public void markMessage(Message message, int status, String errorMessage) {
3423		final int c = message.getStatus();
3424		if (status == Message.STATUS_SEND_FAILED && (c == Message.STATUS_SEND_RECEIVED || c == Message.STATUS_SEND_DISPLAYED)) {
3425			return;
3426		}
3427		if (status == Message.STATUS_SEND_RECEIVED && c == Message.STATUS_SEND_DISPLAYED) {
3428			return;
3429		}
3430		message.setErrorMessage(errorMessage);
3431		message.setStatus(status);
3432		databaseBackend.updateMessage(message, false);
3433		updateConversationUi();
3434	}
3435
3436	private SharedPreferences getPreferences() {
3437		return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
3438	}
3439
3440	public long getAutomaticMessageDeletionDate() {
3441		final long timeout = getLongPreference(SettingsActivity.AUTOMATIC_MESSAGE_DELETION, R.integer.automatic_message_deletion);
3442		return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000));
3443	}
3444
3445	public long getLongPreference(String name, @IntegerRes int res) {
3446		long defaultValue = getResources().getInteger(res);
3447		try {
3448			return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue)));
3449		} catch (NumberFormatException e) {
3450			return defaultValue;
3451		}
3452	}
3453
3454	public boolean getBooleanPreference(String name, @BoolRes int res) {
3455		return getPreferences().getBoolean(name, getResources().getBoolean(res));
3456	}
3457
3458	public boolean confirmMessages() {
3459		return getBooleanPreference("confirm_messages", R.bool.confirm_messages);
3460	}
3461
3462	public boolean allowMessageCorrection() {
3463		return getBooleanPreference("allow_message_correction", R.bool.allow_message_correction);
3464	}
3465
3466	public boolean sendChatStates() {
3467		return getBooleanPreference("chat_states", R.bool.chat_states);
3468	}
3469
3470	private boolean synchronizeWithBookmarks() {
3471		return getBooleanPreference("autojoin", R.bool.autojoin);
3472	}
3473
3474	public boolean indicateReceived() {
3475		return getBooleanPreference("indicate_received", R.bool.indicate_received);
3476	}
3477
3478	public boolean useTorToConnect() {
3479		return QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor);
3480	}
3481
3482	public boolean showExtendedConnectionOptions() {
3483		return QuickConversationsService.isConversations() && getBooleanPreference("show_connection_options", R.bool.show_connection_options);
3484	}
3485
3486	public boolean broadcastLastActivity() {
3487		return getBooleanPreference(SettingsActivity.BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
3488	}
3489
3490	public int unreadCount() {
3491		int count = 0;
3492		for (Conversation conversation : getConversations()) {
3493			count += conversation.unreadCount();
3494		}
3495		return count;
3496	}
3497
3498
3499	private <T> List<T> threadSafeList(Set<T> set) {
3500		synchronized (LISTENER_LOCK) {
3501			return set.size() == 0 ? Collections.emptyList() : new ArrayList<>(set);
3502		}
3503	}
3504
3505	public void showErrorToastInUi(int resId) {
3506		for (OnShowErrorToast listener : threadSafeList(this.mOnShowErrorToasts)) {
3507			listener.onShowErrorToast(resId);
3508		}
3509	}
3510
3511	public void updateConversationUi() {
3512		for (OnConversationUpdate listener : threadSafeList(this.mOnConversationUpdates)) {
3513			listener.onConversationUpdate();
3514		}
3515	}
3516
3517	public void updateAccountUi() {
3518		for (OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) {
3519			listener.onAccountUpdate();
3520		}
3521	}
3522
3523	public void updateRosterUi() {
3524		for (OnRosterUpdate listener : threadSafeList(this.mOnRosterUpdates)) {
3525			listener.onRosterUpdate();
3526		}
3527	}
3528
3529	public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
3530		if (mOnCaptchaRequested.size() > 0) {
3531			DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
3532			Bitmap scaled = Bitmap.createScaledBitmap(captcha, (int) (captcha.getWidth() * metrics.scaledDensity),
3533					(int) (captcha.getHeight() * metrics.scaledDensity), false);
3534			for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
3535				listener.onCaptchaRequested(account, id, data, scaled);
3536			}
3537			return true;
3538		}
3539		return false;
3540	}
3541
3542	public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
3543		for (OnUpdateBlocklist listener : threadSafeList(this.mOnUpdateBlocklist)) {
3544			listener.OnUpdateBlocklist(status);
3545		}
3546	}
3547
3548	public void updateMucRosterUi() {
3549		for (OnMucRosterUpdate listener : threadSafeList(this.mOnMucRosterUpdate)) {
3550			listener.onMucRosterUpdate();
3551		}
3552	}
3553
3554	public void keyStatusUpdated(AxolotlService.FetchStatus report) {
3555		for (OnKeyStatusUpdated listener : threadSafeList(this.mOnKeyStatusUpdated)) {
3556			listener.onKeyStatusUpdated(report);
3557		}
3558	}
3559
3560	public Account findAccountByJid(final Jid accountJid) {
3561		for (Account account : this.accounts) {
3562			if (account.getJid().asBareJid().equals(accountJid.asBareJid())) {
3563				return account;
3564			}
3565		}
3566		return null;
3567	}
3568
3569	public Account findAccountByUuid(final String uuid) {
3570		for(Account account : this.accounts) {
3571			if (account.getUuid().equals(uuid)) {
3572				return account;
3573			}
3574		}
3575		return null;
3576	}
3577
3578	public Conversation findConversationByUuid(String uuid) {
3579		for (Conversation conversation : getConversations()) {
3580			if (conversation.getUuid().equals(uuid)) {
3581				return conversation;
3582			}
3583		}
3584		return null;
3585	}
3586
3587	public Conversation findUniqueConversationByJid(XmppUri xmppUri) {
3588		List<Conversation> findings = new ArrayList<>();
3589		for (Conversation c : getConversations()) {
3590			if (c.getAccount().isEnabled() && c.getJid().asBareJid().equals(xmppUri.getJid()) && ((c.getMode() == Conversational.MODE_MULTI) == xmppUri.isAction(XmppUri.ACTION_JOIN))) {
3591				findings.add(c);
3592			}
3593		}
3594		return findings.size() == 1 ? findings.get(0) : null;
3595	}
3596
3597	public boolean markRead(final Conversation conversation, boolean dismiss) {
3598		return markRead(conversation, null, dismiss).size() > 0;
3599	}
3600
3601	public void markRead(final Conversation conversation) {
3602		markRead(conversation, null, true);
3603	}
3604
3605	public List<Message> markRead(final Conversation conversation, String upToUuid, boolean dismiss) {
3606		if (dismiss) {
3607			mNotificationService.clear(conversation);
3608		}
3609		final List<Message> readMessages = conversation.markRead(upToUuid);
3610		if (readMessages.size() > 0) {
3611			Runnable runnable = () -> {
3612				for (Message message : readMessages) {
3613					databaseBackend.updateMessage(message, false);
3614				}
3615			};
3616			mDatabaseWriterExecutor.execute(runnable);
3617			updateUnreadCountBadge();
3618			return readMessages;
3619		} else {
3620			return readMessages;
3621		}
3622	}
3623
3624	public synchronized void updateUnreadCountBadge() {
3625		int count = unreadCount();
3626		if (unreadCount != count) {
3627			Log.d(Config.LOGTAG, "update unread count to " + count);
3628			if (count > 0) {
3629				ShortcutBadger.applyCount(getApplicationContext(), count);
3630			} else {
3631				ShortcutBadger.removeCount(getApplicationContext());
3632			}
3633			unreadCount = count;
3634		}
3635	}
3636
3637	public void sendReadMarker(final Conversation conversation, String upToUuid) {
3638		final boolean isPrivateAndNonAnonymousMuc = conversation.getMode() == Conversation.MODE_MULTI && conversation.isPrivateAndNonAnonymous();
3639		final List<Message> readMessages = this.markRead(conversation, upToUuid, true);
3640		if (readMessages.size() > 0) {
3641			updateConversationUi();
3642		}
3643		final Message markable = Conversation.getLatestMarkableMessage(readMessages, isPrivateAndNonAnonymousMuc);
3644		if (confirmMessages()
3645				&& markable != null
3646				&& (markable.trusted() || isPrivateAndNonAnonymousMuc)
3647				&& markable.getRemoteMsgId() != null) {
3648			Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
3649			Account account = conversation.getAccount();
3650			final Jid to = markable.getCounterpart();
3651			final boolean groupChat = conversation.getMode() == Conversation.MODE_MULTI;
3652			MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId(), markable.getCounterpart(), groupChat);
3653			this.sendMessagePacket(conversation.getAccount(), packet);
3654		}
3655	}
3656
3657	public SecureRandom getRNG() {
3658		return this.mRandom;
3659	}
3660
3661	public MemorizingTrustManager getMemorizingTrustManager() {
3662		return this.mMemorizingTrustManager;
3663	}
3664
3665	public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
3666		this.mMemorizingTrustManager = trustManager;
3667	}
3668
3669	public void updateMemorizingTrustmanager() {
3670		final MemorizingTrustManager tm;
3671		final boolean dontTrustSystemCAs = getBooleanPreference("dont_trust_system_cas", R.bool.dont_trust_system_cas);
3672		if (dontTrustSystemCAs) {
3673			tm = new MemorizingTrustManager(getApplicationContext(), null);
3674		} else {
3675			tm = new MemorizingTrustManager(getApplicationContext());
3676		}
3677		setMemorizingTrustManager(tm);
3678	}
3679
3680	public LruCache<String, Bitmap> getBitmapCache() {
3681		return this.mBitmapCache;
3682	}
3683
3684	public Collection<String> getKnownHosts() {
3685		final Set<String> hosts = new HashSet<>();
3686		for (final Account account : getAccounts()) {
3687			hosts.add(account.getServer());
3688			for (final Contact contact : account.getRoster().getContacts()) {
3689				if (contact.showInRoster()) {
3690					final String server = contact.getServer();
3691					if (server != null) {
3692						hosts.add(server);
3693					}
3694				}
3695			}
3696		}
3697		if (Config.QUICKSY_DOMAIN != null) {
3698		    hosts.remove(Config.QUICKSY_DOMAIN); //we only want to show this when we type a e164 number
3699        }
3700		if (Config.DOMAIN_LOCK != null) {
3701			hosts.add(Config.DOMAIN_LOCK);
3702		}
3703		if (Config.MAGIC_CREATE_DOMAIN != null) {
3704			hosts.add(Config.MAGIC_CREATE_DOMAIN);
3705		}
3706		return hosts;
3707	}
3708
3709	public Collection<String> getKnownConferenceHosts() {
3710		final Set<String> mucServers = new HashSet<>();
3711		for (final Account account : accounts) {
3712			if (account.getXmppConnection() != null) {
3713				mucServers.addAll(account.getXmppConnection().getMucServers());
3714				for (Bookmark bookmark : account.getBookmarks()) {
3715					final Jid jid = bookmark.getJid();
3716					final String s = jid == null ? null : jid.getDomain();
3717					if (s != null) {
3718						mucServers.add(s);
3719					}
3720				}
3721			}
3722		}
3723		return mucServers;
3724	}
3725
3726	public void sendMessagePacket(Account account, MessagePacket packet) {
3727		XmppConnection connection = account.getXmppConnection();
3728		if (connection != null) {
3729			connection.sendMessagePacket(packet);
3730		}
3731	}
3732
3733	public void sendPresencePacket(Account account, PresencePacket packet) {
3734		XmppConnection connection = account.getXmppConnection();
3735		if (connection != null) {
3736			connection.sendPresencePacket(packet);
3737		}
3738	}
3739
3740	public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
3741		final XmppConnection connection = account.getXmppConnection();
3742		if (connection != null) {
3743			IqPacket request = mIqGenerator.generateCreateAccountWithCaptcha(account, id, data);
3744			connection.sendUnmodifiedIqPacket(request, connection.registrationResponseListener, true);
3745		}
3746	}
3747
3748	public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
3749		final XmppConnection connection = account.getXmppConnection();
3750		if (connection != null) {
3751			connection.sendIqPacket(packet, callback);
3752		} else if (callback != null) {
3753		    callback.onIqPacketReceived(account,new IqPacket(IqPacket.TYPE.TIMEOUT));
3754        }
3755	}
3756
3757	public void sendPresence(final Account account) {
3758		sendPresence(account, checkListeners() && broadcastLastActivity());
3759	}
3760
3761	private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
3762		Presence.Status status;
3763		if (manuallyChangePresence()) {
3764			status = account.getPresenceStatus();
3765		} else {
3766			status = getTargetPresence();
3767		}
3768		PresencePacket packet = mPresenceGenerator.selfPresence(account, status);
3769		String message = account.getPresenceStatusMessage();
3770		if (message != null && !message.isEmpty()) {
3771			packet.addChild(new Element("status").setContent(message));
3772		}
3773		if (mLastActivity > 0 && includeIdleTimestamp) {
3774			long since = Math.min(mLastActivity, System.currentTimeMillis()); //don't send future dates
3775			packet.addChild("idle", Namespace.IDLE).setAttribute("since", AbstractGenerator.getTimestamp(since));
3776		}
3777		sendPresencePacket(account, packet);
3778	}
3779
3780	private void deactivateGracePeriod() {
3781		for (Account account : getAccounts()) {
3782			account.deactivateGracePeriod();
3783		}
3784	}
3785
3786	public void refreshAllPresences() {
3787		boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
3788		for (Account account : getAccounts()) {
3789			if (account.isEnabled()) {
3790				sendPresence(account, includeIdleTimestamp);
3791			}
3792		}
3793	}
3794
3795	private void refreshAllFcmTokens() {
3796		for (Account account : getAccounts()) {
3797			if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
3798				mPushManagementService.registerPushTokenOnServer(account);
3799			}
3800		}
3801	}
3802
3803	private void sendOfflinePresence(final Account account) {
3804		Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence");
3805		sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
3806	}
3807
3808	public MessageGenerator getMessageGenerator() {
3809		return this.mMessageGenerator;
3810	}
3811
3812	public PresenceGenerator getPresenceGenerator() {
3813		return this.mPresenceGenerator;
3814	}
3815
3816	public IqGenerator getIqGenerator() {
3817		return this.mIqGenerator;
3818	}
3819
3820	public IqParser getIqParser() {
3821		return this.mIqParser;
3822	}
3823
3824	public JingleConnectionManager getJingleConnectionManager() {
3825		return this.mJingleConnectionManager;
3826	}
3827
3828	public MessageArchiveService getMessageArchiveService() {
3829		return this.mMessageArchiveService;
3830	}
3831
3832	public QuickConversationsService getQuickConversationsService() {
3833        return this.mQuickConversationsService;
3834    }
3835
3836	public List<Contact> findContacts(Jid jid, String accountJid) {
3837		ArrayList<Contact> contacts = new ArrayList<>();
3838		for (Account account : getAccounts()) {
3839			if ((account.isEnabled() || accountJid != null)
3840					&& (accountJid == null || accountJid.equals(account.getJid().asBareJid().toString()))) {
3841				Contact contact = account.getRoster().getContactFromContactList(jid);
3842				if (contact != null) {
3843					contacts.add(contact);
3844				}
3845			}
3846		}
3847		return contacts;
3848	}
3849
3850	public Conversation findFirstMuc(Jid jid) {
3851		for (Conversation conversation : getConversations()) {
3852			if (conversation.getAccount().isEnabled() && conversation.getJid().asBareJid().equals(jid.asBareJid()) && conversation.getMode() == Conversation.MODE_MULTI) {
3853				return conversation;
3854			}
3855		}
3856		return null;
3857	}
3858
3859	public NotificationService getNotificationService() {
3860		return this.mNotificationService;
3861	}
3862
3863	public HttpConnectionManager getHttpConnectionManager() {
3864		return this.mHttpConnectionManager;
3865	}
3866
3867	public void resendFailedMessages(final Message message) {
3868		final Collection<Message> messages = new ArrayList<>();
3869		Message current = message;
3870		while (current.getStatus() == Message.STATUS_SEND_FAILED) {
3871			messages.add(current);
3872			if (current.mergeable(current.next())) {
3873				current = current.next();
3874			} else {
3875				break;
3876			}
3877		}
3878		for (final Message msg : messages) {
3879			msg.setTime(System.currentTimeMillis());
3880			markMessage(msg, Message.STATUS_WAITING);
3881			this.resendMessage(msg, false);
3882		}
3883		if (message.getConversation() instanceof Conversation) {
3884			((Conversation) message.getConversation()).sort();
3885		}
3886		updateConversationUi();
3887	}
3888
3889	public void clearConversationHistory(final Conversation conversation) {
3890		final long clearDate;
3891		final String reference;
3892		if (conversation.countMessages() > 0) {
3893			Message latestMessage = conversation.getLatestMessage();
3894			clearDate = latestMessage.getTimeSent() + 1000;
3895			reference = latestMessage.getServerMsgId();
3896		} else {
3897			clearDate = System.currentTimeMillis();
3898			reference = null;
3899		}
3900		conversation.clearMessages();
3901		conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam
3902		conversation.setLastClearHistory(clearDate, reference);
3903		Runnable runnable = () -> {
3904			databaseBackend.deleteMessagesInConversation(conversation);
3905			databaseBackend.updateConversation(conversation);
3906		};
3907		mDatabaseWriterExecutor.execute(runnable);
3908	}
3909
3910	public boolean sendBlockRequest(final Blockable blockable, boolean reportSpam) {
3911		if (blockable != null && blockable.getBlockedJid() != null) {
3912			final Jid jid = blockable.getBlockedJid();
3913			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam), new OnIqPacketReceived() {
3914
3915				@Override
3916				public void onIqPacketReceived(final Account account, final IqPacket packet) {
3917					if (packet.getType() == IqPacket.TYPE.RESULT) {
3918						account.getBlocklist().add(jid);
3919						updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
3920					}
3921				}
3922			});
3923			if (removeBlockedConversations(blockable.getAccount(), jid)) {
3924				updateConversationUi();
3925				return true;
3926			} else {
3927				return false;
3928			}
3929		} else {
3930			return false;
3931		}
3932	}
3933
3934	public boolean removeBlockedConversations(final Account account, final Jid blockedJid) {
3935		boolean removed = false;
3936		synchronized (this.conversations) {
3937			boolean domainJid = blockedJid.getLocal() == null;
3938			for (Conversation conversation : this.conversations) {
3939				boolean jidMatches = (domainJid && blockedJid.getDomain().equals(conversation.getJid().getDomain()))
3940						|| blockedJid.equals(conversation.getJid().asBareJid());
3941				if (conversation.getAccount() == account
3942						&& conversation.getMode() == Conversation.MODE_SINGLE
3943						&& jidMatches) {
3944					this.conversations.remove(conversation);
3945					markRead(conversation);
3946					conversation.setStatus(Conversation.STATUS_ARCHIVED);
3947					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": archiving conversation " + conversation.getJid().asBareJid() + " because jid was blocked");
3948					updateConversation(conversation);
3949					removed = true;
3950				}
3951			}
3952		}
3953		return removed;
3954	}
3955
3956	public void sendUnblockRequest(final Blockable blockable) {
3957		if (blockable != null && blockable.getJid() != null) {
3958			final Jid jid = blockable.getBlockedJid();
3959			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() {
3960				@Override
3961				public void onIqPacketReceived(final Account account, final IqPacket packet) {
3962					if (packet.getType() == IqPacket.TYPE.RESULT) {
3963						account.getBlocklist().remove(jid);
3964						updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
3965					}
3966				}
3967			});
3968		}
3969	}
3970
3971	public void publishDisplayName(Account account) {
3972		String displayName = account.getDisplayName();
3973		final IqPacket request;
3974		if (TextUtils.isEmpty(displayName)) {
3975            request = mIqGenerator.deleteNode(Namespace.NICK);
3976		} else {
3977            request = mIqGenerator.publishNick(displayName);
3978        }
3979        mAvatarService.clear(account);
3980        sendIqPacket(account, request, (account1, packet) -> {
3981            if (packet.getType() == IqPacket.TYPE.ERROR) {
3982                Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": unable to modify nick name "+packet.toString());
3983            }
3984        });
3985	}
3986
3987	public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String, String> key) {
3988		ServiceDiscoveryResult result = discoCache.get(key);
3989		if (result != null) {
3990			return result;
3991		} else {
3992			result = databaseBackend.findDiscoveryResult(key.first, key.second);
3993			if (result != null) {
3994				discoCache.put(key, result);
3995			}
3996			return result;
3997		}
3998	}
3999
4000	public void fetchCaps(Account account, final Jid jid, final Presence presence) {
4001		final Pair<String, String> key = new Pair<>(presence.getHash(), presence.getVer());
4002		ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key);
4003		if (disco != null) {
4004			presence.setServiceDiscoveryResult(disco);
4005		} else {
4006			if (!account.inProgressDiscoFetches.contains(key)) {
4007				account.inProgressDiscoFetches.add(key);
4008				IqPacket request = new IqPacket(IqPacket.TYPE.GET);
4009				request.setTo(jid);
4010				final String node = presence.getNode();
4011				final String ver = presence.getVer();
4012				final Element query = request.query("http://jabber.org/protocol/disco#info");
4013				if (node != null && ver != null) {
4014					query.setAttribute("node",node+"#"+ver);
4015				}
4016				Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": making disco request for " + key.second + " to " + jid);
4017				sendIqPacket(account, request, (a, response) -> {
4018					if (response.getType() == IqPacket.TYPE.RESULT) {
4019						ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response);
4020						if (presence.getVer().equals(discoveryResult.getVer())) {
4021							databaseBackend.insertDiscoveryResult(discoveryResult);
4022							injectServiceDiscoveryResult(a.getRoster(), presence.getHash(), presence.getVer(), discoveryResult);
4023						} else {
4024							Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer());
4025						}
4026					}
4027					a.inProgressDiscoFetches.remove(key);
4028				});
4029			}
4030		}
4031	}
4032
4033	private void injectServiceDiscoveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) {
4034		for (Contact contact : roster.getContacts()) {
4035			for (Presence presence : contact.getPresences().getPresences().values()) {
4036				if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
4037					presence.setServiceDiscoveryResult(disco);
4038				}
4039			}
4040		}
4041	}
4042
4043	public void fetchMamPreferences(Account account, final OnMamPreferencesFetched callback) {
4044		final MessageArchiveService.Version version = MessageArchiveService.Version.get(account);
4045		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
4046		request.addChild("prefs", version.namespace);
4047		sendIqPacket(account, request, (account1, packet) -> {
4048			Element prefs = packet.findChild("prefs", version.namespace);
4049			if (packet.getType() == IqPacket.TYPE.RESULT && prefs != null) {
4050				callback.onPreferencesFetched(prefs);
4051			} else {
4052				callback.onPreferencesFetchFailed();
4053			}
4054		});
4055	}
4056
4057	public PushManagementService getPushManagementService() {
4058		return mPushManagementService;
4059	}
4060
4061	public void changeStatus(Account account, PresenceTemplate template, String signature) {
4062		if (!template.getStatusMessage().isEmpty()) {
4063			databaseBackend.insertPresenceTemplate(template);
4064		}
4065		account.setPgpSignature(signature);
4066		account.setPresenceStatus(template.getStatus());
4067		account.setPresenceStatusMessage(template.getStatusMessage());
4068		databaseBackend.updateAccount(account);
4069		sendPresence(account);
4070	}
4071
4072	public List<PresenceTemplate> getPresenceTemplates(Account account) {
4073		List<PresenceTemplate> templates = databaseBackend.getPresenceTemplates();
4074		for (PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) {
4075			if (!templates.contains(template)) {
4076				templates.add(0, template);
4077			}
4078		}
4079		return templates;
4080	}
4081
4082	public void saveConversationAsBookmark(Conversation conversation, String name) {
4083		Account account = conversation.getAccount();
4084		Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid());
4085		if (!conversation.getJid().isBareJid()) {
4086			bookmark.setNick(conversation.getJid().getResource());
4087		}
4088		if (!TextUtils.isEmpty(name)) {
4089			bookmark.setBookmarkName(name);
4090		}
4091		bookmark.setAutojoin(getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin)));
4092		account.getBookmarks().add(bookmark);
4093		pushBookmarks(account);
4094		bookmark.setConversation(conversation);
4095	}
4096
4097	public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
4098		boolean performedVerification = false;
4099		final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
4100		for (XmppUri.Fingerprint fp : fingerprints) {
4101			if (fp.type == XmppUri.FingerprintType.OMEMO) {
4102				String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
4103				FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
4104				if (fingerprintStatus != null) {
4105					if (!fingerprintStatus.isVerified()) {
4106						performedVerification = true;
4107						axolotlService.setFingerprintTrust(fingerprint, fingerprintStatus.toVerified());
4108					}
4109				} else {
4110					axolotlService.preVerifyFingerprint(contact, fingerprint);
4111				}
4112			}
4113		}
4114		return performedVerification;
4115	}
4116
4117	public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
4118		final AxolotlService axolotlService = account.getAxolotlService();
4119		boolean verifiedSomething = false;
4120		for (XmppUri.Fingerprint fp : fingerprints) {
4121			if (fp.type == XmppUri.FingerprintType.OMEMO) {
4122				String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
4123				Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint);
4124				FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
4125				if (fingerprintStatus != null) {
4126					if (!fingerprintStatus.isVerified()) {
4127						axolotlService.setFingerprintTrust(fingerprint, fingerprintStatus.toVerified());
4128						verifiedSomething = true;
4129					}
4130				} else {
4131					axolotlService.preVerifyFingerprint(account, fingerprint);
4132					verifiedSomething = true;
4133				}
4134			}
4135		}
4136		return verifiedSomething;
4137	}
4138
4139	public boolean blindTrustBeforeVerification() {
4140		return getBooleanPreference(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv);
4141	}
4142
4143	public ShortcutService getShortcutService() {
4144		return mShortcutService;
4145	}
4146
4147	public void pushMamPreferences(Account account, Element prefs) {
4148		IqPacket set = new IqPacket(IqPacket.TYPE.SET);
4149		set.addChild(prefs);
4150		sendIqPacket(account, set, null);
4151	}
4152
4153	public interface OnMamPreferencesFetched {
4154		void onPreferencesFetched(Element prefs);
4155
4156		void onPreferencesFetchFailed();
4157	}
4158
4159	public interface OnAccountCreated {
4160		void onAccountCreated(Account account);
4161
4162		void informUser(int r);
4163	}
4164
4165	public interface OnMoreMessagesLoaded {
4166		void onMoreMessagesLoaded(int count, Conversation conversation);
4167
4168		void informUser(int r);
4169	}
4170
4171	public interface OnAccountPasswordChanged {
4172		void onPasswordChangeSucceeded();
4173
4174		void onPasswordChangeFailed();
4175	}
4176
4177    public interface OnRoomDestroy {
4178        void onRoomDestroySucceeded();
4179
4180        void onRoomDestroyFailed();
4181    }
4182
4183	public interface OnAffiliationChanged {
4184		void onAffiliationChangedSuccessful(Jid jid);
4185
4186		void onAffiliationChangeFailed(Jid jid, int resId);
4187	}
4188
4189	public interface OnRoleChanged {
4190		void onRoleChangedSuccessful(String nick);
4191
4192		void onRoleChangeFailed(String nick, int resid);
4193	}
4194
4195	public interface OnConversationUpdate {
4196		void onConversationUpdate();
4197	}
4198
4199	public interface OnAccountUpdate {
4200		void onAccountUpdate();
4201	}
4202
4203	public interface OnCaptchaRequested {
4204		void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha);
4205	}
4206
4207	public interface OnRosterUpdate {
4208		void onRosterUpdate();
4209	}
4210
4211	public interface OnMucRosterUpdate {
4212		void onMucRosterUpdate();
4213	}
4214
4215	public interface OnConferenceConfigurationFetched {
4216		void onConferenceConfigurationFetched(Conversation conversation);
4217
4218		void onFetchFailed(Conversation conversation, Element error);
4219	}
4220
4221	public interface OnConferenceJoined {
4222		void onConferenceJoined(Conversation conversation);
4223	}
4224
4225	public interface OnConfigurationPushed {
4226		void onPushSucceeded();
4227
4228		void onPushFailed();
4229	}
4230
4231	public interface OnShowErrorToast {
4232		void onShowErrorToast(int resId);
4233	}
4234
4235	public class XmppConnectionBinder extends Binder {
4236		public XmppConnectionService getService() {
4237			return XmppConnectionService.this;
4238		}
4239	}
4240
4241	private class InternalEventReceiver extends BroadcastReceiver {
4242
4243        @Override
4244        public void onReceive(Context context, Intent intent) {
4245            onStartCommand(intent,0,0);
4246        }
4247    }
4248}