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