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