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