XmppConnectionService.java

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