XmppConnectionService.java

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