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					synchronized (account.inProgressConferenceJoins) {
2541                        account.inProgressConferenceJoins.remove(conversation);
2542                        sendUnsentMessages(conversation);
2543                    }
2544				}
2545
2546				@Override
2547				public void onConferenceConfigurationFetched(Conversation conversation) {
2548				    if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
2549				        Log.d(Config.LOGTAG,account.getJid().asBareJid()+": conversation ("+conversation.getJid()+") got archived before IQ result");
2550				        return;
2551                    }
2552					join(conversation);
2553				}
2554
2555				@Override
2556				public void onFetchFailed(final Conversation conversation, Element error) {
2557                    if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
2558                        Log.d(Config.LOGTAG,account.getJid().asBareJid()+": conversation ("+conversation.getJid()+") got archived before IQ result");
2559
2560                        return;
2561                    }
2562					if (error != null && "remote-server-not-found".equals(error.getName())) {
2563					    synchronized (account.inProgressConferenceJoins) {
2564                            account.inProgressConferenceJoins.remove(conversation);
2565                        }
2566						conversation.getMucOptions().setError(MucOptions.Error.SERVER_NOT_FOUND);
2567						updateConversationUi();
2568					} else {
2569						join(conversation);
2570						fetchConferenceConfiguration(conversation);
2571					}
2572				}
2573			});
2574			updateConversationUi();
2575		} else {
2576			account.pendingConferenceJoins.add(conversation);
2577			conversation.resetMucOptions();
2578			conversation.setHasMessagesLeftOnServer(false);
2579			updateConversationUi();
2580		}
2581	}
2582
2583	private void fetchConferenceMembers(final Conversation conversation) {
2584		final Account account = conversation.getAccount();
2585		final AxolotlService axolotlService = account.getAxolotlService();
2586		final String[] affiliations = {"member", "admin", "owner"};
2587		OnIqPacketReceived callback = new OnIqPacketReceived() {
2588
2589			private int i = 0;
2590			private boolean success = true;
2591
2592			@Override
2593			public void onIqPacketReceived(Account account, IqPacket packet) {
2594				final boolean omemoEnabled = conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL;
2595				Element query = packet.query("http://jabber.org/protocol/muc#admin");
2596				if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
2597					for (Element child : query.getChildren()) {
2598						if ("item".equals(child.getName())) {
2599							MucOptions.User user = AbstractParser.parseItem(conversation, child);
2600							if (!user.realJidMatchesAccount()) {
2601								boolean isNew = conversation.getMucOptions().updateUser(user);
2602								Contact contact = user.getContact();
2603								if (omemoEnabled
2604										&& isNew
2605										&& user.getRealJid() != null
2606										&& (contact == null || !contact.mutualPresenceSubscription())
2607										&& axolotlService.hasEmptyDeviceList(user.getRealJid())) {
2608									axolotlService.fetchDeviceIds(user.getRealJid());
2609								}
2610							}
2611						}
2612					}
2613				} else {
2614					success = false;
2615					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not request affiliation " + affiliations[i] + " in " + conversation.getJid().asBareJid());
2616				}
2617				++i;
2618				if (i >= affiliations.length) {
2619					List<Jid> members = conversation.getMucOptions().getMembers(true);
2620					if (success) {
2621						List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
2622						boolean changed = false;
2623						for (ListIterator<Jid> iterator = cryptoTargets.listIterator(); iterator.hasNext(); ) {
2624							Jid jid = iterator.next();
2625							if (!members.contains(jid) && !members.contains(Jid.ofDomain(jid.getDomain()))) {
2626								iterator.remove();
2627								Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName());
2628								changed = true;
2629							}
2630						}
2631						if (changed) {
2632							conversation.setAcceptedCryptoTargets(cryptoTargets);
2633							updateConversation(conversation);
2634						}
2635					}
2636					getAvatarService().clear(conversation);
2637					updateMucRosterUi();
2638					updateConversationUi();
2639				}
2640			}
2641		};
2642		for (String affiliation : affiliations) {
2643			sendIqPacket(account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
2644		}
2645		Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching members for " + conversation.getName());
2646	}
2647
2648	public void providePasswordForMuc(Conversation conversation, String password) {
2649		if (conversation.getMode() == Conversation.MODE_MULTI) {
2650			conversation.getMucOptions().setPassword(password);
2651			if (conversation.getBookmark() != null) {
2652				if (synchronizeWithBookmarks()) {
2653					conversation.getBookmark().setAutojoin(true);
2654				}
2655				pushBookmarks(conversation.getAccount());
2656			}
2657			updateConversation(conversation);
2658			joinMuc(conversation);
2659		}
2660	}
2661
2662	private boolean hasEnabledAccounts() {
2663	    if (this.accounts == null) {
2664	        return false;
2665	    }
2666	    for (Account account : this.accounts) {
2667	        if (account.isEnabled()) {
2668	            return true;
2669	        }
2670	    }
2671	    return false;
2672	}
2673
2674
2675	public void getAttachments(final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) {
2676        getAttachments(conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded);
2677    }
2678
2679    public void getAttachments(final Account account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) {
2680        getAttachments(account.getUuid(),jid.asBareJid(),limit, onMediaLoaded);
2681    }
2682
2683
2684	public void getAttachments(final String account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) {
2685        new Thread(() -> onMediaLoaded.onMediaLoaded(fileBackend.convertToAttachments(databaseBackend.getRelativeFilePaths(account, jid, limit)))).start();
2686    }
2687
2688	public void persistSelfNick(MucOptions.User self) {
2689		final Conversation conversation = self.getConversation();
2690		final boolean tookProposedNickFromBookmark = conversation.getMucOptions().isTookProposedNickFromBookmark();
2691		Jid full = self.getFullJid();
2692		if (!full.equals(conversation.getJid())) {
2693			Log.d(Config.LOGTAG, "nick changed. updating");
2694			conversation.setContactJid(full);
2695			databaseBackend.updateConversation(conversation);
2696		}
2697
2698		final Bookmark bookmark = conversation.getBookmark();
2699		final String bookmarkedNick = bookmark == null ? null : bookmark.getNick();
2700        if (bookmark != null && (tookProposedNickFromBookmark || TextUtils.isEmpty(bookmarkedNick)) && !full.getResource().equals(bookmarkedNick)) {
2701            Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": persist nick '" + full.getResource() + "' into bookmark for " + conversation.getJid().asBareJid());
2702            bookmark.setNick(full.getResource());
2703            pushBookmarks(bookmark.getAccount());
2704        }
2705	}
2706
2707	public boolean renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
2708		final MucOptions options = conversation.getMucOptions();
2709		final Jid joinJid = options.createJoinJid(nick);
2710		if (joinJid == null) {
2711			return false;
2712		}
2713		if (options.online()) {
2714			Account account = conversation.getAccount();
2715			options.setOnRenameListener(new OnRenameListener() {
2716
2717				@Override
2718				public void onSuccess() {
2719					callback.success(conversation);
2720				}
2721
2722				@Override
2723				public void onFailure() {
2724					callback.error(R.string.nick_in_use, conversation);
2725				}
2726			});
2727
2728			PresencePacket packet = new PresencePacket();
2729			packet.setTo(joinJid);
2730			packet.setFrom(conversation.getAccount().getJid());
2731
2732			String sig = account.getPgpSignature();
2733			if (sig != null) {
2734				packet.addChild("status").setContent("online");
2735				packet.addChild("x", "jabber:x:signed").setContent(sig);
2736			}
2737			sendPresencePacket(account, packet);
2738		} else {
2739			conversation.setContactJid(joinJid);
2740			databaseBackend.updateConversation(conversation);
2741			if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
2742				Bookmark bookmark = conversation.getBookmark();
2743				if (bookmark != null) {
2744					bookmark.setNick(nick);
2745					pushBookmarks(bookmark.getAccount());
2746				}
2747				joinMuc(conversation);
2748			}
2749		}
2750		return true;
2751	}
2752
2753	public void leaveMuc(Conversation conversation) {
2754		leaveMuc(conversation, false);
2755	}
2756
2757	private void leaveMuc(Conversation conversation, boolean now) {
2758		Account account = conversation.getAccount();
2759		account.pendingConferenceJoins.remove(conversation);
2760		account.pendingConferenceLeaves.remove(conversation);
2761		if (account.getStatus() == Account.State.ONLINE || now) {
2762			sendPresencePacket(conversation.getAccount(), mPresenceGenerator.leave(conversation.getMucOptions()));
2763			conversation.getMucOptions().setOffline();
2764			Bookmark bookmark = conversation.getBookmark();
2765			if (bookmark != null) {
2766				bookmark.setConversation(null);
2767			}
2768			Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": leaving muc " + conversation.getJid());
2769		} else {
2770			account.pendingConferenceLeaves.add(conversation);
2771		}
2772	}
2773
2774	public String findConferenceServer(final Account account) {
2775		String server;
2776		if (account.getXmppConnection() != null) {
2777			server = account.getXmppConnection().getMucServer();
2778			if (server != null) {
2779				return server;
2780			}
2781		}
2782		for (Account other : getAccounts()) {
2783			if (other != account && other.getXmppConnection() != null) {
2784				server = other.getXmppConnection().getMucServer();
2785				if (server != null) {
2786					return server;
2787				}
2788			}
2789		}
2790		return null;
2791	}
2792
2793
2794	public void createPublicChannel(final Account account, final String name, final Jid address, final UiCallback<Conversation> callback) {
2795        joinMuc(findOrCreateConversation(account, address, true, false, true), conversation -> {
2796            final Bundle configuration = IqGenerator.defaultChannelConfiguration();
2797            if (!TextUtils.isEmpty(name)) {
2798                configuration.putString("muc#roomconfig_roomname", name);
2799            }
2800            pushConferenceConfiguration(conversation, configuration, new OnConfigurationPushed() {
2801                @Override
2802                public void onPushSucceeded() {
2803                    saveConversationAsBookmark(conversation, name);
2804                    callback.success(conversation);
2805                }
2806
2807                @Override
2808                public void onPushFailed() {
2809                    if (conversation.getMucOptions().getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
2810                        callback.error(R.string.unable_to_set_channel_configuration, conversation);
2811                    } else {
2812                        callback.error(R.string.joined_an_existing_channel, conversation);
2813                    }
2814                }
2815            });
2816        });
2817    }
2818
2819	public boolean createAdhocConference(final Account account,
2820	                                     final String name,
2821	                                     final Iterable<Jid> jids,
2822	                                     final UiCallback<Conversation> callback) {
2823		Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": creating adhoc conference with " + jids.toString());
2824		if (account.getStatus() == Account.State.ONLINE) {
2825			try {
2826				String server = findConferenceServer(account);
2827				if (server == null) {
2828					if (callback != null) {
2829						callback.error(R.string.no_conference_server_found, null);
2830					}
2831					return false;
2832				}
2833				final Jid jid = Jid.of(CryptoHelper.pronounceable(getRNG()), server, null);
2834				final Conversation conversation = findOrCreateConversation(account, jid, true, false, true);
2835				joinMuc(conversation, new OnConferenceJoined() {
2836					@Override
2837					public void onConferenceJoined(final Conversation conversation) {
2838						final Bundle configuration = IqGenerator.defaultGroupChatConfiguration();
2839						if (!TextUtils.isEmpty(name)) {
2840							configuration.putString("muc#roomconfig_roomname", name);
2841						}
2842						pushConferenceConfiguration(conversation, configuration, new OnConfigurationPushed() {
2843							@Override
2844							public void onPushSucceeded() {
2845								for (Jid invite : jids) {
2846									invite(conversation, invite);
2847								}
2848								if (account.countPresences() > 1) {
2849									directInvite(conversation, account.getJid().asBareJid());
2850								}
2851								saveConversationAsBookmark(conversation, name);
2852								if (callback != null) {
2853									callback.success(conversation);
2854								}
2855							}
2856
2857							@Override
2858							public void onPushFailed() {
2859								archiveConversation(conversation);
2860								if (callback != null) {
2861									callback.error(R.string.conference_creation_failed, conversation);
2862								}
2863							}
2864						});
2865					}
2866				});
2867				return true;
2868			} catch (IllegalArgumentException e) {
2869				if (callback != null) {
2870					callback.error(R.string.conference_creation_failed, null);
2871				}
2872				return false;
2873			}
2874		} else {
2875			if (callback != null) {
2876				callback.error(R.string.not_connected_try_again, null);
2877			}
2878			return false;
2879		}
2880	}
2881
2882	public void fetchConferenceConfiguration(final Conversation conversation) {
2883		fetchConferenceConfiguration(conversation, null);
2884	}
2885
2886	public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) {
2887		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2888		request.setTo(conversation.getJid().asBareJid());
2889		request.query("http://jabber.org/protocol/disco#info");
2890		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
2891			@Override
2892			public void onIqPacketReceived(Account account, IqPacket packet) {
2893				if (packet.getType() == IqPacket.TYPE.RESULT) {
2894
2895					final MucOptions mucOptions = conversation.getMucOptions();
2896					final Bookmark bookmark = conversation.getBookmark();
2897					final boolean sameBefore = StringUtils.equals(bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName());
2898
2899					if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(packet))) {
2900						Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid());
2901						updateConversation(conversation);
2902					}
2903
2904					if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) {
2905						if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) {
2906							pushBookmarks(account);
2907						}
2908					}
2909
2910
2911					if (callback != null) {
2912						callback.onConferenceConfigurationFetched(conversation);
2913					}
2914
2915
2916
2917					updateConversationUi();
2918				} else if (packet.getType() == IqPacket.TYPE.ERROR) {
2919					if (callback != null) {
2920						callback.onFetchFailed(conversation, packet.getError());
2921					}
2922				}
2923			}
2924		});
2925	}
2926
2927	public void pushNodeConfiguration(Account account, final String node, final Bundle options, final OnConfigurationPushed callback) {
2928		pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback);
2929	}
2930
2931	public void pushNodeConfiguration(Account account, final Jid jid, final String node, final Bundle options, final OnConfigurationPushed callback) {
2932        Log.d(Config.LOGTAG,"pushing node configuration");
2933		sendIqPacket(account, mIqGenerator.requestPubsubConfiguration(jid, node), new OnIqPacketReceived() {
2934			@Override
2935			public void onIqPacketReceived(Account account, IqPacket packet) {
2936				if (packet.getType() == IqPacket.TYPE.RESULT) {
2937					Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub#owner");
2938					Element configuration = pubsub == null ? null : pubsub.findChild("configure");
2939					Element x = configuration == null ? null : configuration.findChild("x", Namespace.DATA);
2940					if (x != null) {
2941						Data data = Data.parse(x);
2942						data.submit(options);
2943						sendIqPacket(account, mIqGenerator.publishPubsubConfiguration(jid, node, data), new OnIqPacketReceived() {
2944							@Override
2945							public void onIqPacketReceived(Account account, IqPacket packet) {
2946								if (packet.getType() == IqPacket.TYPE.RESULT && callback != null) {
2947									Log.d(Config.LOGTAG,account.getJid().asBareJid()+": successfully changed node configuration for node "+node);
2948									callback.onPushSucceeded();
2949								} else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) {
2950									callback.onPushFailed();
2951								}
2952							}
2953						});
2954					} else if (callback != null) {
2955						callback.onPushFailed();
2956					}
2957				} else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) {
2958					callback.onPushFailed();
2959				}
2960			}
2961		});
2962	}
2963
2964	public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConfigurationPushed callback) {
2965	    if (options.getString("muc#roomconfig_whois","moderators").equals("anyone")) {
2966	        conversation.setAttribute("accept_non_anonymous",true);
2967            updateConversation(conversation);
2968        }
2969		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2970		request.setTo(conversation.getJid().asBareJid());
2971		request.query("http://jabber.org/protocol/muc#owner");
2972		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
2973			@Override
2974			public void onIqPacketReceived(Account account, IqPacket packet) {
2975				if (packet.getType() == IqPacket.TYPE.RESULT) {
2976					Data data = Data.parse(packet.query().findChild("x", Namespace.DATA));
2977					data.submit(options);
2978					Log.d(Config.LOGTAG,data.toString());
2979					IqPacket set = new IqPacket(IqPacket.TYPE.SET);
2980					set.setTo(conversation.getJid().asBareJid());
2981					set.query("http://jabber.org/protocol/muc#owner").addChild(data);
2982					sendIqPacket(account, set, new OnIqPacketReceived() {
2983						@Override
2984						public void onIqPacketReceived(Account account, IqPacket packet) {
2985							if (callback != null) {
2986								if (packet.getType() == IqPacket.TYPE.RESULT) {
2987									callback.onPushSucceeded();
2988								} else {
2989									callback.onPushFailed();
2990								}
2991							}
2992						}
2993					});
2994				} else {
2995					if (callback != null) {
2996						callback.onPushFailed();
2997					}
2998				}
2999			}
3000		});
3001	}
3002
3003	public void pushSubjectToConference(final Conversation conference, final String subject) {
3004		MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, StringUtils.nullOnEmpty(subject));
3005		this.sendMessagePacket(conference.getAccount(), packet);
3006	}
3007
3008	public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
3009		final Jid jid = user.asBareJid();
3010		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
3011		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
3012			@Override
3013			public void onIqPacketReceived(Account account, IqPacket packet) {
3014				if (packet.getType() == IqPacket.TYPE.RESULT) {
3015					conference.getMucOptions().changeAffiliation(jid, affiliation);
3016					getAvatarService().clear(conference);
3017					callback.onAffiliationChangedSuccessful(jid);
3018				} else {
3019					callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
3020				}
3021			}
3022		});
3023	}
3024
3025	public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
3026		List<Jid> jids = new ArrayList<>();
3027		for (MucOptions.User user : conference.getMucOptions().getUsers()) {
3028			if (user.getAffiliation() == before && user.getRealJid() != null) {
3029				jids.add(user.getRealJid());
3030			}
3031		}
3032		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
3033		sendIqPacket(conference.getAccount(), request, mDefaultIqHandler);
3034	}
3035
3036	public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role) {
3037		IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
3038		Log.d(Config.LOGTAG, request.toString());
3039		sendIqPacket(conference.getAccount(), request, (account, packet) -> {
3040            if (packet.getType() != IqPacket.TYPE.RESULT) {
3041                Log.d(Config.LOGTAG,account.getJid().asBareJid()+" unable to change role of "+nick);
3042            }
3043        });
3044	}
3045
3046    public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) {
3047        IqPacket request = new IqPacket(IqPacket.TYPE.SET);
3048        request.setTo(conversation.getJid().asBareJid());
3049        request.query("http://jabber.org/protocol/muc#owner").addChild("destroy");
3050        sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
3051            @Override
3052            public void onIqPacketReceived(Account account, IqPacket packet) {
3053                if (packet.getType() == IqPacket.TYPE.RESULT) {
3054                    if (callback != null) {
3055                        callback.onRoomDestroySucceeded();
3056                    }
3057                } else if (packet.getType() == IqPacket.TYPE.ERROR) {
3058                    if (callback != null) {
3059                        callback.onRoomDestroyFailed();
3060                    }
3061                }
3062            }
3063        });
3064    }
3065
3066	private void disconnect(Account account, boolean force) {
3067		if ((account.getStatus() == Account.State.ONLINE)
3068				|| (account.getStatus() == Account.State.DISABLED)) {
3069			final XmppConnection connection = account.getXmppConnection();
3070			if (!force) {
3071				List<Conversation> conversations = getConversations();
3072				for (Conversation conversation : conversations) {
3073					if (conversation.getAccount() == account) {
3074						if (conversation.getMode() == Conversation.MODE_MULTI) {
3075							leaveMuc(conversation, true);
3076						}
3077					}
3078				}
3079				sendOfflinePresence(account);
3080			}
3081			connection.disconnect(force);
3082		}
3083	}
3084
3085	@Override
3086	public IBinder onBind(Intent intent) {
3087		return mBinder;
3088	}
3089
3090	public void updateMessage(Message message) {
3091		updateMessage(message, true);
3092	}
3093
3094	public void updateMessage(Message message, boolean includeBody) {
3095		databaseBackend.updateMessage(message, includeBody);
3096		updateConversationUi();
3097	}
3098
3099	public void updateMessage(Message message, String uuid) {
3100		if (!databaseBackend.updateMessage(message, uuid)) {
3101            Log.e(Config.LOGTAG,"error updated message in DB after edit");
3102        }
3103		updateConversationUi();
3104	}
3105
3106	protected void syncDirtyContacts(Account account) {
3107		for (Contact contact : account.getRoster().getContacts()) {
3108			if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
3109				pushContactToServer(contact);
3110			}
3111			if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
3112				deleteContactOnServer(contact);
3113			}
3114		}
3115	}
3116
3117	public void createContact(Contact contact, boolean autoGrant) {
3118		if (autoGrant) {
3119			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
3120			contact.setOption(Contact.Options.ASKING);
3121		}
3122		pushContactToServer(contact);
3123	}
3124
3125	public void pushContactToServer(final Contact contact) {
3126		contact.resetOption(Contact.Options.DIRTY_DELETE);
3127		contact.setOption(Contact.Options.DIRTY_PUSH);
3128		final Account account = contact.getAccount();
3129		if (account.getStatus() == Account.State.ONLINE) {
3130			final boolean ask = contact.getOption(Contact.Options.ASKING);
3131			final boolean sendUpdates = contact
3132					.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
3133					&& contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
3134			final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
3135			iq.query(Namespace.ROSTER).addChild(contact.asElement());
3136			account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
3137			if (sendUpdates) {
3138				sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
3139			}
3140			if (ask) {
3141				sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact));
3142			}
3143		} else {
3144			syncRoster(contact.getAccount());
3145		}
3146	}
3147
3148	public void publishMucAvatar(final Conversation conversation, final Uri image, final OnAvatarPublication callback) {
3149		new Thread(() -> {
3150			final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
3151			final int size = Config.AVATAR_SIZE;
3152			final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
3153			if (avatar != null) {
3154				if (!getFileBackend().save(avatar)) {
3155					callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
3156					return;
3157				}
3158				avatar.owner = conversation.getJid().asBareJid();
3159				publishMucAvatar(conversation, avatar, callback);
3160			} else {
3161				callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
3162			}
3163		}).start();
3164	}
3165
3166	public void publishAvatar(final Account account, final Uri image, final OnAvatarPublication callback) {
3167		new Thread(() -> {
3168			final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
3169			final int size = Config.AVATAR_SIZE;
3170			final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
3171			if (avatar != null) {
3172				if (!getFileBackend().save(avatar)) {
3173					Log.d(Config.LOGTAG,"unable to save vcard");
3174					callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
3175					return;
3176				}
3177				publishAvatar(account, avatar, callback);
3178			} else {
3179				callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
3180			}
3181		}).start();
3182
3183	}
3184
3185	private void publishMucAvatar(Conversation conversation, Avatar avatar, OnAvatarPublication callback) {
3186		final IqPacket retrieve = mIqGenerator.retrieveVcardAvatar(avatar);
3187		sendIqPacket(conversation.getAccount(), retrieve, (account, response) -> {
3188			boolean itemNotFound = response.getType() == IqPacket.TYPE.ERROR && response.hasChild("error") && response.findChild("error").hasChild("item-not-found");
3189			if (response.getType() == IqPacket.TYPE.RESULT || itemNotFound) {
3190				Element vcard = response.findChild("vCard", "vcard-temp");
3191				if (vcard == null) {
3192					vcard = new Element("vCard", "vcard-temp");
3193				}
3194				Element photo = vcard.findChild("PHOTO");
3195				if (photo == null) {
3196					photo = vcard.addChild("PHOTO");
3197				}
3198				photo.clearChildren();
3199				photo.addChild("TYPE").setContent(avatar.type);
3200				photo.addChild("BINVAL").setContent(avatar.image);
3201				IqPacket publication = new IqPacket(IqPacket.TYPE.SET);
3202				publication.setTo(conversation.getJid().asBareJid());
3203				publication.addChild(vcard);
3204				sendIqPacket(account, publication, (a1, publicationResponse) -> {
3205					if (publicationResponse.getType() == IqPacket.TYPE.RESULT) {
3206						callback.onAvatarPublicationSucceeded();
3207					} else {
3208						Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getError());
3209						callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
3210					}
3211				});
3212			} else {
3213				Log.d(Config.LOGTAG, "failed to request vcard " + response.toString());
3214				callback.onAvatarPublicationFailed(R.string.error_publish_avatar_no_server_support);
3215			}
3216		});
3217	}
3218
3219    public void publishAvatar(Account account, final Avatar avatar, final OnAvatarPublication callback) {
3220        final Bundle options;
3221        if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
3222            options = PublishOptions.openAccess();
3223        } else {
3224            options = null;
3225        }
3226        publishAvatar(account, avatar, options, true, callback);
3227    }
3228
3229	public void publishAvatar(Account account, final Avatar avatar, final Bundle options, final boolean retry, final OnAvatarPublication callback) {
3230        Log.d(Config.LOGTAG,account.getJid().asBareJid()+": publishing avatar. options="+options);
3231		IqPacket packet = this.mIqGenerator.publishAvatar(avatar, options);
3232		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
3233
3234			@Override
3235			public void onIqPacketReceived(Account account, IqPacket result) {
3236				if (result.getType() == IqPacket.TYPE.RESULT) {
3237                    publishAvatarMetadata(account, avatar, options,true, callback);
3238                } else if (retry && PublishOptions.preconditionNotMet(result)) {
3239				    pushNodeConfiguration(account, "urn:xmpp:avatar:data", options, new OnConfigurationPushed() {
3240                        @Override
3241                        public void onPushSucceeded() {
3242                            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": changed node configuration for avatar node");
3243                            publishAvatar(account, avatar, options, false, callback);
3244                        }
3245
3246                        @Override
3247                        public void onPushFailed() {
3248                            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": unable to change node configuration for avatar node");
3249                            publishAvatar(account, avatar, null, false, callback);
3250                        }
3251                    });
3252				} else {
3253					Element error = result.findChild("error");
3254					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server rejected avatar " + (avatar.size / 1024) + "KiB " + (error != null ? error.toString() : ""));
3255					if (callback != null) {
3256						callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
3257					}
3258				}
3259			}
3260		});
3261	}
3262
3263	public void publishAvatarMetadata(Account account, final Avatar avatar, final Bundle options, final boolean retry, final OnAvatarPublication callback) {
3264        final IqPacket packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options);
3265        sendIqPacket(account, packet, new OnIqPacketReceived() {
3266            @Override
3267            public void onIqPacketReceived(Account account, IqPacket result) {
3268                if (result.getType() == IqPacket.TYPE.RESULT) {
3269                    if (account.setAvatar(avatar.getFilename())) {
3270                        getAvatarService().clear(account);
3271                        databaseBackend.updateAccount(account);
3272                        notifyAccountAvatarHasChanged(account);
3273                    }
3274                    Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": published avatar " + (avatar.size / 1024) + "KiB");
3275                    if (callback != null) {
3276                        callback.onAvatarPublicationSucceeded();
3277                    }
3278                } else if (retry && PublishOptions.preconditionNotMet(result)) {
3279                    pushNodeConfiguration(account, "urn:xmpp:avatar:metadata", options, new OnConfigurationPushed() {
3280                        @Override
3281                        public void onPushSucceeded() {
3282                            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": changed node configuration for avatar meta data node");
3283                            publishAvatarMetadata(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 meta data node");
3289                            publishAvatarMetadata(account, avatar,  null,false, callback);
3290                        }
3291                    });
3292                } else {
3293                    if (callback != null) {
3294                        callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
3295                    }
3296                }
3297            }
3298        });
3299    }
3300
3301	public void republishAvatarIfNeeded(Account account) {
3302		if (account.getAxolotlService().isPepBroken()) {
3303			Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping republication of avatar because pep is broken");
3304			return;
3305		}
3306		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
3307		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
3308
3309			private Avatar parseAvatar(IqPacket packet) {
3310				Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
3311				if (pubsub != null) {
3312					Element items = pubsub.findChild("items");
3313					if (items != null) {
3314						return Avatar.parseMetadata(items);
3315					}
3316				}
3317				return null;
3318			}
3319
3320			private boolean errorIsItemNotFound(IqPacket packet) {
3321				Element error = packet.findChild("error");
3322				return packet.getType() == IqPacket.TYPE.ERROR
3323						&& error != null
3324						&& error.hasChild("item-not-found");
3325			}
3326
3327			@Override
3328			public void onIqPacketReceived(Account account, IqPacket packet) {
3329				if (packet.getType() == IqPacket.TYPE.RESULT || errorIsItemNotFound(packet)) {
3330					Avatar serverAvatar = parseAvatar(packet);
3331					if (serverAvatar == null && account.getAvatar() != null) {
3332						Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar());
3333						if (avatar != null) {
3334							Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": avatar on server was null. republishing");
3335							publishAvatar(account, fileBackend.getStoredPepAvatar(account.getAvatar()), null);
3336						} else {
3337							Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": error rereading avatar");
3338						}
3339					}
3340				}
3341			}
3342		});
3343	}
3344
3345	public void fetchAvatar(Account account, Avatar avatar) {
3346		fetchAvatar(account, avatar, null);
3347	}
3348
3349	public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
3350		final String KEY = generateFetchKey(account, avatar);
3351		synchronized (this.mInProgressAvatarFetches) {
3352		    if (mInProgressAvatarFetches.add(KEY)) {
3353                switch (avatar.origin) {
3354                    case PEP:
3355                        this.mInProgressAvatarFetches.add(KEY);
3356                        fetchAvatarPep(account, avatar, callback);
3357                        break;
3358                    case VCARD:
3359                        this.mInProgressAvatarFetches.add(KEY);
3360                        fetchAvatarVcard(account, avatar, callback);
3361                        break;
3362                }
3363            } else if (avatar.origin == Avatar.Origin.PEP) {
3364		        mOmittedPepAvatarFetches.add(KEY);
3365            } else {
3366		        Log.d(Config.LOGTAG,account.getJid().asBareJid()+": already fetching "+avatar.origin+" avatar for "+avatar.owner);
3367            }
3368		}
3369	}
3370
3371	private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
3372		IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar);
3373		sendIqPacket(account, packet, (a, result) -> {
3374			synchronized (mInProgressAvatarFetches) {
3375				mInProgressAvatarFetches.remove(generateFetchKey(a, avatar));
3376			}
3377			final String ERROR = a.getJid().asBareJid() + ": fetching avatar for " + avatar.owner + " failed ";
3378			if (result.getType() == IqPacket.TYPE.RESULT) {
3379				avatar.image = mIqParser.avatarData(result);
3380				if (avatar.image != null) {
3381					if (getFileBackend().save(avatar)) {
3382						if (a.getJid().asBareJid().equals(avatar.owner)) {
3383							if (a.setAvatar(avatar.getFilename())) {
3384								databaseBackend.updateAccount(a);
3385							}
3386							getAvatarService().clear(a);
3387							updateConversationUi();
3388							updateAccountUi();
3389						} else {
3390							Contact contact = a.getRoster().getContact(avatar.owner);
3391							if (contact.setAvatar(avatar)) {
3392								syncRoster(account);
3393								getAvatarService().clear(contact);
3394								updateConversationUi();
3395								updateRosterUi();
3396							}
3397						}
3398						if (callback != null) {
3399							callback.success(avatar);
3400						}
3401						Log.d(Config.LOGTAG, a.getJid().asBareJid()
3402								+ ": successfully fetched pep avatar for " + avatar.owner);
3403						return;
3404					}
3405				} else {
3406
3407					Log.d(Config.LOGTAG, ERROR + "(parsing error)");
3408				}
3409			} else {
3410				Element error = result.findChild("error");
3411				if (error == null) {
3412					Log.d(Config.LOGTAG, ERROR + "(server error)");
3413				} else {
3414					Log.d(Config.LOGTAG, ERROR + error.toString());
3415				}
3416			}
3417			if (callback != null) {
3418				callback.error(0, null);
3419			}
3420
3421		});
3422	}
3423
3424	private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
3425		IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
3426		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
3427			@Override
3428			public void onIqPacketReceived(Account account, IqPacket packet) {
3429			    final boolean previouslyOmittedPepFetch;
3430				synchronized (mInProgressAvatarFetches) {
3431				    final String KEY = generateFetchKey(account, avatar);
3432					mInProgressAvatarFetches.remove(KEY);
3433					previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY);
3434				}
3435				if (packet.getType() == IqPacket.TYPE.RESULT) {
3436					Element vCard = packet.findChild("vCard", "vcard-temp");
3437					Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
3438					String image = photo != null ? photo.findChildContent("BINVAL") : null;
3439					if (image != null) {
3440						avatar.image = image;
3441						if (getFileBackend().save(avatar)) {
3442							Log.d(Config.LOGTAG, account.getJid().asBareJid()
3443									+ ": successfully fetched vCard avatar for " + avatar.owner+" omittedPep="+previouslyOmittedPepFetch);
3444							if (avatar.owner.isBareJid()) {
3445								if (account.getJid().asBareJid().equals(avatar.owner) && account.getAvatar() == null) {
3446									Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": had no avatar. replacing with vcard");
3447									account.setAvatar(avatar.getFilename());
3448									databaseBackend.updateAccount(account);
3449									getAvatarService().clear(account);
3450									updateAccountUi();
3451								} else {
3452									Contact contact = account.getRoster().getContact(avatar.owner);
3453									if (contact.setAvatar(avatar, previouslyOmittedPepFetch)) {
3454										syncRoster(account);
3455										getAvatarService().clear(contact);
3456										updateRosterUi();
3457									}
3458								}
3459								updateConversationUi();
3460							} else {
3461								Conversation conversation = find(account, avatar.owner.asBareJid());
3462								if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
3463									MucOptions.User user = conversation.getMucOptions().findUserByFullJid(avatar.owner);
3464									if (user != null) {
3465										if (user.setAvatar(avatar)) {
3466											getAvatarService().clear(user);
3467											updateConversationUi();
3468											updateMucRosterUi();
3469										}
3470										if (user.getRealJid() != null) {
3471										    Contact contact = account.getRoster().getContact(user.getRealJid());
3472										    if (contact.setAvatar(avatar)) {
3473                                                syncRoster(account);
3474                                                getAvatarService().clear(contact);
3475                                                updateRosterUi();
3476                                            }
3477                                        }
3478									}
3479								}
3480							}
3481						}
3482					}
3483				}
3484			}
3485		});
3486	}
3487
3488	public void checkForAvatar(Account account, final UiCallback<Avatar> callback) {
3489		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
3490		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
3491
3492			@Override
3493			public void onIqPacketReceived(Account account, IqPacket packet) {
3494				if (packet.getType() == IqPacket.TYPE.RESULT) {
3495					Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
3496					if (pubsub != null) {
3497						Element items = pubsub.findChild("items");
3498						if (items != null) {
3499							Avatar avatar = Avatar.parseMetadata(items);
3500							if (avatar != null) {
3501								avatar.owner = account.getJid().asBareJid();
3502								if (fileBackend.isAvatarCached(avatar)) {
3503									if (account.setAvatar(avatar.getFilename())) {
3504										databaseBackend.updateAccount(account);
3505									}
3506									getAvatarService().clear(account);
3507									callback.success(avatar);
3508								} else {
3509									fetchAvatarPep(account, avatar, callback);
3510								}
3511								return;
3512							}
3513						}
3514					}
3515				}
3516				callback.error(0, null);
3517			}
3518		});
3519	}
3520
3521	public void notifyAccountAvatarHasChanged(final Account account) {
3522	    final XmppConnection connection = account.getXmppConnection();
3523	    if (connection != null && connection.getFeatures().bookmarksConversion()) {
3524            Log.d(Config.LOGTAG,account.getJid().asBareJid()+": avatar changed. resending presence to online group chats");
3525            for(Conversation conversation : conversations) {
3526                if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) {
3527                    final MucOptions mucOptions = conversation.getMucOptions();
3528                    if (mucOptions.online()) {
3529                        PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous());
3530                        packet.setTo(mucOptions.getSelf().getFullJid());
3531                        connection.sendPresencePacket(packet);
3532                    }
3533                }
3534            }
3535        }
3536    }
3537
3538	public void deleteContactOnServer(Contact contact) {
3539		contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
3540		contact.resetOption(Contact.Options.DIRTY_PUSH);
3541		contact.setOption(Contact.Options.DIRTY_DELETE);
3542		Account account = contact.getAccount();
3543		if (account.getStatus() == Account.State.ONLINE) {
3544			IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
3545			Element item = iq.query(Namespace.ROSTER).addChild("item");
3546			item.setAttribute("jid", contact.getJid().toString());
3547			item.setAttribute("subscription", "remove");
3548			account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
3549		}
3550	}
3551
3552	public void updateConversation(final Conversation conversation) {
3553		mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation));
3554	}
3555
3556	private void reconnectAccount(final Account account, final boolean force, final boolean interactive) {
3557		synchronized (account) {
3558			XmppConnection connection = account.getXmppConnection();
3559			if (connection == null) {
3560				connection = createConnection(account);
3561				account.setXmppConnection(connection);
3562			}
3563			boolean hasInternet = hasInternetConnection();
3564			if (account.isEnabled() && hasInternet) {
3565				if (!force) {
3566					disconnect(account, false);
3567				}
3568				Thread thread = new Thread(connection);
3569				connection.setInteractive(interactive);
3570				connection.prepareNewConnection();
3571				connection.interrupt();
3572				thread.start();
3573				scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
3574			} else {
3575				disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
3576				account.getRoster().clearPresences();
3577				connection.resetEverything();
3578				final AxolotlService axolotlService = account.getAxolotlService();
3579				if (axolotlService != null) {
3580					axolotlService.resetBrokenness();
3581				}
3582				if (!hasInternet) {
3583					account.setStatus(Account.State.NO_INTERNET);
3584				}
3585			}
3586		}
3587	}
3588
3589	public void reconnectAccountInBackground(final Account account) {
3590		new Thread(() -> reconnectAccount(account, false, true)).start();
3591	}
3592
3593	public void invite(Conversation conversation, Jid contact) {
3594		Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": inviting " + contact + " to " + conversation.getJid().asBareJid());
3595		MessagePacket packet = mMessageGenerator.invite(conversation, contact);
3596		sendMessagePacket(conversation.getAccount(), packet);
3597	}
3598
3599	public void directInvite(Conversation conversation, Jid jid) {
3600		MessagePacket packet = mMessageGenerator.directInvite(conversation, jid);
3601		sendMessagePacket(conversation.getAccount(), packet);
3602	}
3603
3604	public void resetSendingToWaiting(Account account) {
3605		for (Conversation conversation : getConversations()) {
3606			if (conversation.getAccount() == account) {
3607				conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
3608			}
3609		}
3610	}
3611
3612	public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) {
3613		return markMessage(account, recipient, uuid, status, null);
3614	}
3615
3616	public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status, String errorMessage) {
3617		if (uuid == null) {
3618			return null;
3619		}
3620		for (Conversation conversation : getConversations()) {
3621			if (conversation.getJid().asBareJid().equals(recipient) && conversation.getAccount() == account) {
3622				final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
3623				if (message != null) {
3624					markMessage(message, status, errorMessage);
3625				}
3626				return message;
3627			}
3628		}
3629		return null;
3630	}
3631
3632	public boolean markMessage(Conversation conversation, String uuid, int status, String serverMessageId) {
3633		if (uuid == null) {
3634			return false;
3635		} else {
3636			Message message = conversation.findSentMessageWithUuid(uuid);
3637			if (message != null) {
3638				if (message.getServerMsgId() == null) {
3639					message.setServerMsgId(serverMessageId);
3640				}
3641				markMessage(message, status);
3642				return true;
3643			} else {
3644				return false;
3645			}
3646		}
3647	}
3648
3649	public void markMessage(Message message, int status) {
3650		markMessage(message, status, null);
3651	}
3652
3653
3654	public void markMessage(Message message, int status, String errorMessage) {
3655		final int c = message.getStatus();
3656		if (status == Message.STATUS_SEND_FAILED && (c == Message.STATUS_SEND_RECEIVED || c == Message.STATUS_SEND_DISPLAYED)) {
3657			return;
3658		}
3659		if (status == Message.STATUS_SEND_RECEIVED && c == Message.STATUS_SEND_DISPLAYED) {
3660			return;
3661		}
3662		message.setErrorMessage(errorMessage);
3663		message.setStatus(status);
3664		databaseBackend.updateMessage(message, false);
3665		updateConversationUi();
3666	}
3667
3668	private SharedPreferences getPreferences() {
3669		return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
3670	}
3671
3672	public long getAutomaticMessageDeletionDate() {
3673		final long timeout = getLongPreference(SettingsActivity.AUTOMATIC_MESSAGE_DELETION, R.integer.automatic_message_deletion);
3674		return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000));
3675	}
3676
3677	public long getLongPreference(String name, @IntegerRes int res) {
3678		long defaultValue = getResources().getInteger(res);
3679		try {
3680			return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue)));
3681		} catch (NumberFormatException e) {
3682			return defaultValue;
3683		}
3684	}
3685
3686	public boolean getBooleanPreference(String name, @BoolRes int res) {
3687		return getPreferences().getBoolean(name, getResources().getBoolean(res));
3688	}
3689
3690	public boolean confirmMessages() {
3691		return getBooleanPreference("confirm_messages", R.bool.confirm_messages);
3692	}
3693
3694	public boolean allowMessageCorrection() {
3695		return getBooleanPreference("allow_message_correction", R.bool.allow_message_correction);
3696	}
3697
3698	public boolean sendChatStates() {
3699		return getBooleanPreference("chat_states", R.bool.chat_states);
3700	}
3701
3702	private boolean synchronizeWithBookmarks() {
3703		return getBooleanPreference("autojoin", R.bool.autojoin);
3704	}
3705
3706	public boolean indicateReceived() {
3707		return getBooleanPreference("indicate_received", R.bool.indicate_received);
3708	}
3709
3710	public boolean useTorToConnect() {
3711		return QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor);
3712	}
3713
3714	public boolean showExtendedConnectionOptions() {
3715		return QuickConversationsService.isConversations() && getBooleanPreference("show_connection_options", R.bool.show_connection_options);
3716	}
3717
3718	public boolean broadcastLastActivity() {
3719		return getBooleanPreference(SettingsActivity.BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
3720	}
3721
3722	public int unreadCount() {
3723		int count = 0;
3724		for (Conversation conversation : getConversations()) {
3725			count += conversation.unreadCount();
3726		}
3727		return count;
3728	}
3729
3730
3731	private <T> List<T> threadSafeList(Set<T> set) {
3732		synchronized (LISTENER_LOCK) {
3733			return set.size() == 0 ? Collections.emptyList() : new ArrayList<>(set);
3734		}
3735	}
3736
3737	public void showErrorToastInUi(int resId) {
3738		for (OnShowErrorToast listener : threadSafeList(this.mOnShowErrorToasts)) {
3739			listener.onShowErrorToast(resId);
3740		}
3741	}
3742
3743	public void updateConversationUi() {
3744		for (OnConversationUpdate listener : threadSafeList(this.mOnConversationUpdates)) {
3745			listener.onConversationUpdate();
3746		}
3747	}
3748
3749	public void updateAccountUi() {
3750		for (OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) {
3751			listener.onAccountUpdate();
3752		}
3753	}
3754
3755	public void updateRosterUi() {
3756		for (OnRosterUpdate listener : threadSafeList(this.mOnRosterUpdates)) {
3757			listener.onRosterUpdate();
3758		}
3759	}
3760
3761	public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
3762		if (mOnCaptchaRequested.size() > 0) {
3763			DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
3764			Bitmap scaled = Bitmap.createScaledBitmap(captcha, (int) (captcha.getWidth() * metrics.scaledDensity),
3765					(int) (captcha.getHeight() * metrics.scaledDensity), false);
3766			for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
3767				listener.onCaptchaRequested(account, id, data, scaled);
3768			}
3769			return true;
3770		}
3771		return false;
3772	}
3773
3774	public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
3775		for (OnUpdateBlocklist listener : threadSafeList(this.mOnUpdateBlocklist)) {
3776			listener.OnUpdateBlocklist(status);
3777		}
3778	}
3779
3780	public void updateMucRosterUi() {
3781		for (OnMucRosterUpdate listener : threadSafeList(this.mOnMucRosterUpdate)) {
3782			listener.onMucRosterUpdate();
3783		}
3784	}
3785
3786	public void keyStatusUpdated(AxolotlService.FetchStatus report) {
3787		for (OnKeyStatusUpdated listener : threadSafeList(this.mOnKeyStatusUpdated)) {
3788			listener.onKeyStatusUpdated(report);
3789		}
3790	}
3791
3792	public Account findAccountByJid(final Jid accountJid) {
3793		for (Account account : this.accounts) {
3794			if (account.getJid().asBareJid().equals(accountJid.asBareJid())) {
3795				return account;
3796			}
3797		}
3798		return null;
3799	}
3800
3801	public Account findAccountByUuid(final String uuid) {
3802		for(Account account : this.accounts) {
3803			if (account.getUuid().equals(uuid)) {
3804				return account;
3805			}
3806		}
3807		return null;
3808	}
3809
3810	public Conversation findConversationByUuid(String uuid) {
3811		for (Conversation conversation : getConversations()) {
3812			if (conversation.getUuid().equals(uuid)) {
3813				return conversation;
3814			}
3815		}
3816		return null;
3817	}
3818
3819	public Conversation findUniqueConversationByJid(XmppUri xmppUri) {
3820		List<Conversation> findings = new ArrayList<>();
3821		for (Conversation c : getConversations()) {
3822			if (c.getAccount().isEnabled() && c.getJid().asBareJid().equals(xmppUri.getJid()) && ((c.getMode() == Conversational.MODE_MULTI) == xmppUri.isAction(XmppUri.ACTION_JOIN))) {
3823				findings.add(c);
3824			}
3825		}
3826		return findings.size() == 1 ? findings.get(0) : null;
3827	}
3828
3829	public boolean markRead(final Conversation conversation, boolean dismiss) {
3830		return markRead(conversation, null, dismiss).size() > 0;
3831	}
3832
3833	public void markRead(final Conversation conversation) {
3834		markRead(conversation, null, true);
3835	}
3836
3837	public List<Message> markRead(final Conversation conversation, String upToUuid, boolean dismiss) {
3838		if (dismiss) {
3839			mNotificationService.clear(conversation);
3840		}
3841		final List<Message> readMessages = conversation.markRead(upToUuid);
3842		if (readMessages.size() > 0) {
3843			Runnable runnable = () -> {
3844				for (Message message : readMessages) {
3845					databaseBackend.updateMessage(message, false);
3846				}
3847			};
3848			mDatabaseWriterExecutor.execute(runnable);
3849			updateUnreadCountBadge();
3850			return readMessages;
3851		} else {
3852			return readMessages;
3853		}
3854	}
3855
3856	public synchronized void updateUnreadCountBadge() {
3857		int count = unreadCount();
3858		if (unreadCount != count) {
3859			Log.d(Config.LOGTAG, "update unread count to " + count);
3860			if (count > 0) {
3861				ShortcutBadger.applyCount(getApplicationContext(), count);
3862			} else {
3863				ShortcutBadger.removeCount(getApplicationContext());
3864			}
3865			unreadCount = count;
3866		}
3867	}
3868
3869	public void sendReadMarker(final Conversation conversation, String upToUuid) {
3870		final boolean isPrivateAndNonAnonymousMuc = conversation.getMode() == Conversation.MODE_MULTI && conversation.isPrivateAndNonAnonymous();
3871		final List<Message> readMessages = this.markRead(conversation, upToUuid, true);
3872		if (readMessages.size() > 0) {
3873			updateConversationUi();
3874		}
3875		final Message markable = Conversation.getLatestMarkableMessage(readMessages, isPrivateAndNonAnonymousMuc);
3876		if (confirmMessages()
3877				&& markable != null
3878				&& (markable.trusted() || isPrivateAndNonAnonymousMuc)
3879				&& markable.getRemoteMsgId() != null) {
3880			Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
3881			Account account = conversation.getAccount();
3882			final Jid to = markable.getCounterpart();
3883			final boolean groupChat = conversation.getMode() == Conversation.MODE_MULTI;
3884			MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId(), markable.getCounterpart(), groupChat);
3885			this.sendMessagePacket(conversation.getAccount(), packet);
3886		}
3887	}
3888
3889	public SecureRandom getRNG() {
3890		return this.mRandom;
3891	}
3892
3893	public MemorizingTrustManager getMemorizingTrustManager() {
3894		return this.mMemorizingTrustManager;
3895	}
3896
3897	public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
3898		this.mMemorizingTrustManager = trustManager;
3899	}
3900
3901	public void updateMemorizingTrustmanager() {
3902		final MemorizingTrustManager tm;
3903		final boolean dontTrustSystemCAs = getBooleanPreference("dont_trust_system_cas", R.bool.dont_trust_system_cas);
3904		if (dontTrustSystemCAs) {
3905			tm = new MemorizingTrustManager(getApplicationContext(), null);
3906		} else {
3907			tm = new MemorizingTrustManager(getApplicationContext());
3908		}
3909		setMemorizingTrustManager(tm);
3910	}
3911
3912	public LruCache<String, Bitmap> getBitmapCache() {
3913		return this.mBitmapCache;
3914	}
3915
3916	public Collection<String> getKnownHosts() {
3917		final Set<String> hosts = new HashSet<>();
3918		for (final Account account : getAccounts()) {
3919			hosts.add(account.getServer());
3920			for (final Contact contact : account.getRoster().getContacts()) {
3921				if (contact.showInRoster()) {
3922					final String server = contact.getServer();
3923					if (server != null) {
3924						hosts.add(server);
3925					}
3926				}
3927			}
3928		}
3929		if (Config.QUICKSY_DOMAIN != null) {
3930		    hosts.remove(Config.QUICKSY_DOMAIN); //we only want to show this when we type a e164 number
3931        }
3932		if (Config.DOMAIN_LOCK != null) {
3933			hosts.add(Config.DOMAIN_LOCK);
3934		}
3935		if (Config.MAGIC_CREATE_DOMAIN != null) {
3936			hosts.add(Config.MAGIC_CREATE_DOMAIN);
3937		}
3938		return hosts;
3939	}
3940
3941	public Collection<String> getKnownConferenceHosts() {
3942		final Set<String> mucServers = new HashSet<>();
3943		for (final Account account : accounts) {
3944			if (account.getXmppConnection() != null) {
3945				mucServers.addAll(account.getXmppConnection().getMucServers());
3946				for (Bookmark bookmark : account.getBookmarks()) {
3947					final Jid jid = bookmark.getJid();
3948					final String s = jid == null ? null : jid.getDomain();
3949					if (s != null) {
3950						mucServers.add(s);
3951					}
3952				}
3953			}
3954		}
3955		return mucServers;
3956	}
3957
3958	public void sendMessagePacket(Account account, MessagePacket packet) {
3959		XmppConnection connection = account.getXmppConnection();
3960		if (connection != null) {
3961			connection.sendMessagePacket(packet);
3962		}
3963	}
3964
3965	public void sendPresencePacket(Account account, PresencePacket packet) {
3966		XmppConnection connection = account.getXmppConnection();
3967		if (connection != null) {
3968			connection.sendPresencePacket(packet);
3969		}
3970	}
3971
3972	public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
3973		final XmppConnection connection = account.getXmppConnection();
3974		if (connection != null) {
3975			IqPacket request = mIqGenerator.generateCreateAccountWithCaptcha(account, id, data);
3976			connection.sendUnmodifiedIqPacket(request, connection.registrationResponseListener, true);
3977		}
3978	}
3979
3980	public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
3981		final XmppConnection connection = account.getXmppConnection();
3982		if (connection != null) {
3983			connection.sendIqPacket(packet, callback);
3984		} else if (callback != null) {
3985		    callback.onIqPacketReceived(account,new IqPacket(IqPacket.TYPE.TIMEOUT));
3986        }
3987	}
3988
3989	public void sendPresence(final Account account) {
3990		sendPresence(account, checkListeners() && broadcastLastActivity());
3991	}
3992
3993	private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
3994		Presence.Status status;
3995		if (manuallyChangePresence()) {
3996			status = account.getPresenceStatus();
3997		} else {
3998			status = getTargetPresence();
3999		}
4000		PresencePacket packet = mPresenceGenerator.selfPresence(account, status);
4001		String message = account.getPresenceStatusMessage();
4002		if (message != null && !message.isEmpty()) {
4003			packet.addChild(new Element("status").setContent(message));
4004		}
4005		if (mLastActivity > 0 && includeIdleTimestamp) {
4006			long since = Math.min(mLastActivity, System.currentTimeMillis()); //don't send future dates
4007			packet.addChild("idle", Namespace.IDLE).setAttribute("since", AbstractGenerator.getTimestamp(since));
4008		}
4009		sendPresencePacket(account, packet);
4010	}
4011
4012	private void deactivateGracePeriod() {
4013		for (Account account : getAccounts()) {
4014			account.deactivateGracePeriod();
4015		}
4016	}
4017
4018	public void refreshAllPresences() {
4019		boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
4020		for (Account account : getAccounts()) {
4021			if (account.isEnabled()) {
4022				sendPresence(account, includeIdleTimestamp);
4023			}
4024		}
4025	}
4026
4027	private void refreshAllFcmTokens() {
4028		for (Account account : getAccounts()) {
4029			if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
4030				mPushManagementService.registerPushTokenOnServer(account);
4031			}
4032		}
4033	}
4034
4035	private void sendOfflinePresence(final Account account) {
4036		Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence");
4037		sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
4038	}
4039
4040	public MessageGenerator getMessageGenerator() {
4041		return this.mMessageGenerator;
4042	}
4043
4044	public PresenceGenerator getPresenceGenerator() {
4045		return this.mPresenceGenerator;
4046	}
4047
4048	public IqGenerator getIqGenerator() {
4049		return this.mIqGenerator;
4050	}
4051
4052	public IqParser getIqParser() {
4053		return this.mIqParser;
4054	}
4055
4056	public JingleConnectionManager getJingleConnectionManager() {
4057		return this.mJingleConnectionManager;
4058	}
4059
4060	public MessageArchiveService getMessageArchiveService() {
4061		return this.mMessageArchiveService;
4062	}
4063
4064	public QuickConversationsService getQuickConversationsService() {
4065        return this.mQuickConversationsService;
4066    }
4067
4068	public List<Contact> findContacts(Jid jid, String accountJid) {
4069		ArrayList<Contact> contacts = new ArrayList<>();
4070		for (Account account : getAccounts()) {
4071			if ((account.isEnabled() || accountJid != null)
4072					&& (accountJid == null || accountJid.equals(account.getJid().asBareJid().toString()))) {
4073				Contact contact = account.getRoster().getContactFromContactList(jid);
4074				if (contact != null) {
4075					contacts.add(contact);
4076				}
4077			}
4078		}
4079		return contacts;
4080	}
4081
4082	public Conversation findFirstMuc(Jid jid) {
4083		for (Conversation conversation : getConversations()) {
4084			if (conversation.getAccount().isEnabled() && conversation.getJid().asBareJid().equals(jid.asBareJid()) && conversation.getMode() == Conversation.MODE_MULTI) {
4085				return conversation;
4086			}
4087		}
4088		return null;
4089	}
4090
4091	public NotificationService getNotificationService() {
4092		return this.mNotificationService;
4093	}
4094
4095	public HttpConnectionManager getHttpConnectionManager() {
4096		return this.mHttpConnectionManager;
4097	}
4098
4099	public void resendFailedMessages(final Message message) {
4100		final Collection<Message> messages = new ArrayList<>();
4101		Message current = message;
4102		while (current.getStatus() == Message.STATUS_SEND_FAILED) {
4103			messages.add(current);
4104			if (current.mergeable(current.next())) {
4105				current = current.next();
4106			} else {
4107				break;
4108			}
4109		}
4110		for (final Message msg : messages) {
4111			msg.setTime(System.currentTimeMillis());
4112			markMessage(msg, Message.STATUS_WAITING);
4113			this.resendMessage(msg, false);
4114		}
4115		if (message.getConversation() instanceof Conversation) {
4116			((Conversation) message.getConversation()).sort();
4117		}
4118		updateConversationUi();
4119	}
4120
4121	public void clearConversationHistory(final Conversation conversation) {
4122		final long clearDate;
4123		final String reference;
4124		if (conversation.countMessages() > 0) {
4125			Message latestMessage = conversation.getLatestMessage();
4126			clearDate = latestMessage.getTimeSent() + 1000;
4127			reference = latestMessage.getServerMsgId();
4128		} else {
4129			clearDate = System.currentTimeMillis();
4130			reference = null;
4131		}
4132		conversation.clearMessages();
4133		conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam
4134		conversation.setLastClearHistory(clearDate, reference);
4135		Runnable runnable = () -> {
4136			databaseBackend.deleteMessagesInConversation(conversation);
4137			databaseBackend.updateConversation(conversation);
4138		};
4139		mDatabaseWriterExecutor.execute(runnable);
4140	}
4141
4142	public boolean sendBlockRequest(final Blockable blockable, boolean reportSpam) {
4143		if (blockable != null && blockable.getBlockedJid() != null) {
4144			final Jid jid = blockable.getBlockedJid();
4145			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam), new OnIqPacketReceived() {
4146
4147				@Override
4148				public void onIqPacketReceived(final Account account, final IqPacket packet) {
4149					if (packet.getType() == IqPacket.TYPE.RESULT) {
4150						account.getBlocklist().add(jid);
4151						updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
4152					}
4153				}
4154			});
4155			if (removeBlockedConversations(blockable.getAccount(), jid)) {
4156				updateConversationUi();
4157				return true;
4158			} else {
4159				return false;
4160			}
4161		} else {
4162			return false;
4163		}
4164	}
4165
4166	public boolean removeBlockedConversations(final Account account, final Jid blockedJid) {
4167		boolean removed = false;
4168		synchronized (this.conversations) {
4169			boolean domainJid = blockedJid.getLocal() == null;
4170			for (Conversation conversation : this.conversations) {
4171				boolean jidMatches = (domainJid && blockedJid.getDomain().equals(conversation.getJid().getDomain()))
4172						|| blockedJid.equals(conversation.getJid().asBareJid());
4173				if (conversation.getAccount() == account
4174						&& conversation.getMode() == Conversation.MODE_SINGLE
4175						&& jidMatches) {
4176					this.conversations.remove(conversation);
4177					markRead(conversation);
4178					conversation.setStatus(Conversation.STATUS_ARCHIVED);
4179					Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": archiving conversation " + conversation.getJid().asBareJid() + " because jid was blocked");
4180					updateConversation(conversation);
4181					removed = true;
4182				}
4183			}
4184		}
4185		return removed;
4186	}
4187
4188	public void sendUnblockRequest(final Blockable blockable) {
4189		if (blockable != null && blockable.getJid() != null) {
4190			final Jid jid = blockable.getBlockedJid();
4191			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() {
4192				@Override
4193				public void onIqPacketReceived(final Account account, final IqPacket packet) {
4194					if (packet.getType() == IqPacket.TYPE.RESULT) {
4195						account.getBlocklist().remove(jid);
4196						updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
4197					}
4198				}
4199			});
4200		}
4201	}
4202
4203	public void publishDisplayName(Account account) {
4204		String displayName = account.getDisplayName();
4205		final IqPacket request;
4206		if (TextUtils.isEmpty(displayName)) {
4207            request = mIqGenerator.deleteNode(Namespace.NICK);
4208		} else {
4209            request = mIqGenerator.publishNick(displayName);
4210        }
4211        mAvatarService.clear(account);
4212        sendIqPacket(account, request, (account1, packet) -> {
4213            if (packet.getType() == IqPacket.TYPE.ERROR) {
4214                Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": unable to modify nick name "+packet.toString());
4215            }
4216        });
4217	}
4218
4219	public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String, String> key) {
4220		ServiceDiscoveryResult result = discoCache.get(key);
4221		if (result != null) {
4222			return result;
4223		} else {
4224			result = databaseBackend.findDiscoveryResult(key.first, key.second);
4225			if (result != null) {
4226				discoCache.put(key, result);
4227			}
4228			return result;
4229		}
4230	}
4231
4232	public void fetchCaps(Account account, final Jid jid, final Presence presence) {
4233		final Pair<String, String> key = new Pair<>(presence.getHash(), presence.getVer());
4234		ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key);
4235		if (disco != null) {
4236			presence.setServiceDiscoveryResult(disco);
4237		} else {
4238			if (!account.inProgressDiscoFetches.contains(key)) {
4239				account.inProgressDiscoFetches.add(key);
4240				IqPacket request = new IqPacket(IqPacket.TYPE.GET);
4241				request.setTo(jid);
4242				final String node = presence.getNode();
4243				final String ver = presence.getVer();
4244				final Element query = request.query("http://jabber.org/protocol/disco#info");
4245				if (node != null && ver != null) {
4246					query.setAttribute("node",node+"#"+ver);
4247				}
4248				Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": making disco request for " + key.second + " to " + jid);
4249				sendIqPacket(account, request, (a, response) -> {
4250					if (response.getType() == IqPacket.TYPE.RESULT) {
4251						ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response);
4252						if (presence.getVer().equals(discoveryResult.getVer())) {
4253							databaseBackend.insertDiscoveryResult(discoveryResult);
4254							injectServiceDiscoveryResult(a.getRoster(), presence.getHash(), presence.getVer(), discoveryResult);
4255						} else {
4256							Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer());
4257						}
4258					}
4259					a.inProgressDiscoFetches.remove(key);
4260				});
4261			}
4262		}
4263	}
4264
4265	private void injectServiceDiscoveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) {
4266		for (Contact contact : roster.getContacts()) {
4267			for (Presence presence : contact.getPresences().getPresences().values()) {
4268				if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
4269					presence.setServiceDiscoveryResult(disco);
4270				}
4271			}
4272		}
4273	}
4274
4275	public void fetchMamPreferences(Account account, final OnMamPreferencesFetched callback) {
4276		final MessageArchiveService.Version version = MessageArchiveService.Version.get(account);
4277		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
4278		request.addChild("prefs", version.namespace);
4279		sendIqPacket(account, request, (account1, packet) -> {
4280			Element prefs = packet.findChild("prefs", version.namespace);
4281			if (packet.getType() == IqPacket.TYPE.RESULT && prefs != null) {
4282				callback.onPreferencesFetched(prefs);
4283			} else {
4284				callback.onPreferencesFetchFailed();
4285			}
4286		});
4287	}
4288
4289	public PushManagementService getPushManagementService() {
4290		return mPushManagementService;
4291	}
4292
4293	public void changeStatus(Account account, PresenceTemplate template, String signature) {
4294		if (!template.getStatusMessage().isEmpty()) {
4295			databaseBackend.insertPresenceTemplate(template);
4296		}
4297		account.setPgpSignature(signature);
4298		account.setPresenceStatus(template.getStatus());
4299		account.setPresenceStatusMessage(template.getStatusMessage());
4300		databaseBackend.updateAccount(account);
4301		sendPresence(account);
4302	}
4303
4304	public List<PresenceTemplate> getPresenceTemplates(Account account) {
4305		List<PresenceTemplate> templates = databaseBackend.getPresenceTemplates();
4306		for (PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) {
4307			if (!templates.contains(template)) {
4308				templates.add(0, template);
4309			}
4310		}
4311		return templates;
4312	}
4313
4314	public void saveConversationAsBookmark(Conversation conversation, String name) {
4315		Account account = conversation.getAccount();
4316		Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid());
4317		if (!conversation.getJid().isBareJid()) {
4318			bookmark.setNick(conversation.getJid().getResource());
4319		}
4320		if (!TextUtils.isEmpty(name)) {
4321			bookmark.setBookmarkName(name);
4322		}
4323		bookmark.setAutojoin(getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin)));
4324		account.getBookmarks().add(bookmark);
4325		pushBookmarks(account);
4326		bookmark.setConversation(conversation);
4327	}
4328
4329	public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
4330		boolean performedVerification = false;
4331		final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
4332		for (XmppUri.Fingerprint fp : fingerprints) {
4333			if (fp.type == XmppUri.FingerprintType.OMEMO) {
4334				String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
4335				FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
4336				if (fingerprintStatus != null) {
4337					if (!fingerprintStatus.isVerified()) {
4338						performedVerification = true;
4339						axolotlService.setFingerprintTrust(fingerprint, fingerprintStatus.toVerified());
4340					}
4341				} else {
4342					axolotlService.preVerifyFingerprint(contact, fingerprint);
4343				}
4344			}
4345		}
4346		return performedVerification;
4347	}
4348
4349	public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
4350		final AxolotlService axolotlService = account.getAxolotlService();
4351		boolean verifiedSomething = false;
4352		for (XmppUri.Fingerprint fp : fingerprints) {
4353			if (fp.type == XmppUri.FingerprintType.OMEMO) {
4354				String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
4355				Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint);
4356				FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
4357				if (fingerprintStatus != null) {
4358					if (!fingerprintStatus.isVerified()) {
4359						axolotlService.setFingerprintTrust(fingerprint, fingerprintStatus.toVerified());
4360						verifiedSomething = true;
4361					}
4362				} else {
4363					axolotlService.preVerifyFingerprint(account, fingerprint);
4364					verifiedSomething = true;
4365				}
4366			}
4367		}
4368		return verifiedSomething;
4369	}
4370
4371	public boolean blindTrustBeforeVerification() {
4372		return getBooleanPreference(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv);
4373	}
4374
4375	public ShortcutService getShortcutService() {
4376		return mShortcutService;
4377	}
4378
4379	public void pushMamPreferences(Account account, Element prefs) {
4380		IqPacket set = new IqPacket(IqPacket.TYPE.SET);
4381		set.addChild(prefs);
4382		sendIqPacket(account, set, null);
4383	}
4384
4385	public interface OnMamPreferencesFetched {
4386		void onPreferencesFetched(Element prefs);
4387
4388		void onPreferencesFetchFailed();
4389	}
4390
4391	public interface OnAccountCreated {
4392		void onAccountCreated(Account account);
4393
4394		void informUser(int r);
4395	}
4396
4397	public interface OnMoreMessagesLoaded {
4398		void onMoreMessagesLoaded(int count, Conversation conversation);
4399
4400		void informUser(int r);
4401	}
4402
4403	public interface OnAccountPasswordChanged {
4404		void onPasswordChangeSucceeded();
4405
4406		void onPasswordChangeFailed();
4407	}
4408
4409    public interface OnRoomDestroy {
4410        void onRoomDestroySucceeded();
4411
4412        void onRoomDestroyFailed();
4413    }
4414
4415	public interface OnAffiliationChanged {
4416		void onAffiliationChangedSuccessful(Jid jid);
4417
4418		void onAffiliationChangeFailed(Jid jid, int resId);
4419	}
4420
4421	public interface OnConversationUpdate {
4422		void onConversationUpdate();
4423	}
4424
4425	public interface OnAccountUpdate {
4426		void onAccountUpdate();
4427	}
4428
4429	public interface OnCaptchaRequested {
4430		void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha);
4431	}
4432
4433	public interface OnRosterUpdate {
4434		void onRosterUpdate();
4435	}
4436
4437	public interface OnMucRosterUpdate {
4438		void onMucRosterUpdate();
4439	}
4440
4441	public interface OnConferenceConfigurationFetched {
4442		void onConferenceConfigurationFetched(Conversation conversation);
4443
4444		void onFetchFailed(Conversation conversation, Element error);
4445	}
4446
4447	public interface OnConferenceJoined {
4448		void onConferenceJoined(Conversation conversation);
4449	}
4450
4451	public interface OnConfigurationPushed {
4452		void onPushSucceeded();
4453
4454		void onPushFailed();
4455	}
4456
4457	public interface OnShowErrorToast {
4458		void onShowErrorToast(int resId);
4459	}
4460
4461	public class XmppConnectionBinder extends Binder {
4462		public XmppConnectionService getService() {
4463			return XmppConnectionService.this;
4464		}
4465	}
4466
4467	private class InternalEventReceiver extends BroadcastReceiver {
4468
4469        @Override
4470        public void onReceive(Context context, Intent intent) {
4471            onStartCommand(intent,0,0);
4472        }
4473    }
4474}