XmppConnectionService.java

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