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.app.AlarmManager;
   9import android.app.KeyguardManager;
  10import android.app.Notification;
  11import android.app.NotificationManager;
  12import android.app.PendingIntent;
  13import android.app.Service;
  14import android.content.BroadcastReceiver;
  15import android.content.ComponentName;
  16import android.content.Context;
  17import android.content.Intent;
  18import android.content.IntentFilter;
  19import android.content.SharedPreferences;
  20import android.content.pm.PackageManager;
  21import android.content.pm.ServiceInfo;
  22import android.database.ContentObserver;
  23import android.graphics.Bitmap;
  24import android.graphics.drawable.AnimatedImageDrawable;
  25import android.graphics.drawable.BitmapDrawable;
  26import android.graphics.drawable.Drawable;
  27import android.media.AudioManager;
  28import android.net.ConnectivityManager;
  29import android.net.Network;
  30import android.net.NetworkCapabilities;
  31import android.net.NetworkInfo;
  32import android.net.Uri;
  33import android.os.Binder;
  34import android.os.Build;
  35import android.os.Bundle;
  36import android.os.Environment;
  37import android.os.IBinder;
  38import android.os.Messenger;
  39import android.os.PowerManager;
  40import android.os.PowerManager.WakeLock;
  41import android.os.SystemClock;
  42import android.preference.PreferenceManager;
  43import android.provider.ContactsContract;
  44import android.provider.DocumentsContract;
  45import android.security.KeyChain;
  46import android.text.TextUtils;
  47import android.util.DisplayMetrics;
  48import android.util.Log;
  49import android.util.LruCache;
  50import android.util.Pair;
  51import androidx.annotation.BoolRes;
  52import androidx.annotation.IntegerRes;
  53import androidx.annotation.NonNull;
  54import androidx.annotation.Nullable;
  55import androidx.core.app.RemoteInput;
  56import androidx.core.content.ContextCompat;
  57
  58import com.cheogram.android.EmojiSearch;
  59import com.cheogram.android.WebxdcUpdate;
  60
  61import com.google.common.base.Objects;
  62import com.google.common.base.Optional;
  63import com.google.common.base.Strings;
  64import com.google.common.collect.Collections2;
  65import com.google.common.collect.ImmutableMap;
  66import com.google.common.collect.ImmutableSet;
  67import com.google.common.collect.Iterables;
  68import com.google.common.collect.Maps;
  69import com.google.common.collect.Multimap;
  70import com.google.common.io.Files;
  71
  72import com.kedia.ogparser.JsoupProxy;
  73import com.kedia.ogparser.OpenGraphCallback;
  74import com.kedia.ogparser.OpenGraphParser;
  75import com.kedia.ogparser.OpenGraphResult;
  76
  77import org.conscrypt.Conscrypt;
  78import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep;
  79import org.openintents.openpgp.IOpenPgpService2;
  80import org.openintents.openpgp.util.OpenPgpApi;
  81import org.openintents.openpgp.util.OpenPgpServiceConnection;
  82
  83import java.io.File;
  84import java.io.FileInputStream;
  85import java.io.IOException;
  86import java.net.URI;
  87import java.security.Security;
  88import java.security.cert.CertificateException;
  89import java.security.cert.X509Certificate;
  90import java.util.ArrayList;
  91import java.util.Arrays;
  92import java.util.Collection;
  93import java.util.Collections;
  94import java.util.HashMap;
  95import java.util.HashSet;
  96import java.util.Hashtable;
  97import java.util.Iterator;
  98import java.util.List;
  99import java.util.ListIterator;
 100import java.util.Map;
 101import java.util.Set;
 102import java.util.WeakHashMap;
 103import java.util.concurrent.CopyOnWriteArrayList;
 104import java.util.concurrent.CountDownLatch;
 105import java.util.concurrent.Executor;
 106import java.util.concurrent.Executors;
 107import java.util.concurrent.Semaphore;
 108import java.util.concurrent.RejectedExecutionException;
 109import java.util.concurrent.ScheduledExecutorService;
 110import java.util.concurrent.TimeUnit;
 111import java.util.concurrent.atomic.AtomicBoolean;
 112import java.util.concurrent.atomic.AtomicLong;
 113import java.util.concurrent.atomic.AtomicReference;
 114import java.util.function.Consumer;
 115
 116import io.ipfs.cid.Cid;
 117
 118import eu.siacs.conversations.AppSettings;
 119import eu.siacs.conversations.Config;
 120import eu.siacs.conversations.R;
 121import eu.siacs.conversations.android.JabberIdContact;
 122import eu.siacs.conversations.crypto.OmemoSetting;
 123import eu.siacs.conversations.crypto.PgpDecryptionService;
 124import eu.siacs.conversations.crypto.PgpEngine;
 125import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 126import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 127import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
 128import eu.siacs.conversations.entities.Account;
 129import eu.siacs.conversations.entities.Blockable;
 130import eu.siacs.conversations.entities.Bookmark;
 131import eu.siacs.conversations.entities.Contact;
 132import eu.siacs.conversations.entities.Conversation;
 133import eu.siacs.conversations.entities.Conversational;
 134import eu.siacs.conversations.entities.DownloadableFile;
 135import eu.siacs.conversations.entities.Message;
 136import eu.siacs.conversations.entities.MucOptions;
 137import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
 138import eu.siacs.conversations.entities.Presence;
 139import eu.siacs.conversations.entities.PresenceTemplate;
 140import eu.siacs.conversations.entities.Reaction;
 141import eu.siacs.conversations.entities.Roster;
 142import eu.siacs.conversations.entities.ServiceDiscoveryResult;
 143import eu.siacs.conversations.generator.AbstractGenerator;
 144import eu.siacs.conversations.generator.IqGenerator;
 145import eu.siacs.conversations.generator.MessageGenerator;
 146import eu.siacs.conversations.generator.PresenceGenerator;
 147import eu.siacs.conversations.http.HttpConnectionManager;
 148import eu.siacs.conversations.parser.AbstractParser;
 149import eu.siacs.conversations.parser.IqParser;
 150import eu.siacs.conversations.persistance.DatabaseBackend;
 151import eu.siacs.conversations.persistance.FileBackend;
 152import eu.siacs.conversations.persistance.UnifiedPushDatabase;
 153import eu.siacs.conversations.receiver.SystemEventReceiver;
 154import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity;
 155import eu.siacs.conversations.ui.ConversationsActivity;
 156import eu.siacs.conversations.ui.RtpSessionActivity;
 157import eu.siacs.conversations.ui.UiCallback;
 158import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
 159import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
 160import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
 161import eu.siacs.conversations.ui.util.QuoteHelper;
 162import eu.siacs.conversations.utils.AccountUtils;
 163import eu.siacs.conversations.utils.Compatibility;
 164import eu.siacs.conversations.utils.ConversationsFileObserver;
 165import eu.siacs.conversations.utils.CryptoHelper;
 166import eu.siacs.conversations.utils.Emoticons;
 167import eu.siacs.conversations.utils.EasyOnboardingInvite;
 168import eu.siacs.conversations.utils.ExceptionHelper;
 169import eu.siacs.conversations.utils.FileUtils;
 170import eu.siacs.conversations.utils.MessageUtils;
 171import eu.siacs.conversations.utils.Emoticons;
 172import eu.siacs.conversations.utils.MimeUtils;
 173import eu.siacs.conversations.utils.PhoneHelper;
 174import eu.siacs.conversations.utils.QuickLoader;
 175import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
 176import eu.siacs.conversations.utils.ReplacingTaskManager;
 177import eu.siacs.conversations.utils.Resolver;
 178import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
 179import eu.siacs.conversations.utils.StringUtils;
 180import eu.siacs.conversations.utils.TorServiceUtils;
 181import eu.siacs.conversations.utils.ThemeHelper;
 182import eu.siacs.conversations.utils.WakeLockHelper;
 183import eu.siacs.conversations.utils.XmppUri;
 184import eu.siacs.conversations.xml.Element;
 185import eu.siacs.conversations.xml.LocalizedContent;
 186import eu.siacs.conversations.xml.Namespace;
 187import eu.siacs.conversations.xmpp.Jid;
 188import eu.siacs.conversations.xmpp.OnContactStatusChanged;
 189import eu.siacs.conversations.xmpp.OnGatewayResult;
 190import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 191import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
 192import eu.siacs.conversations.xmpp.OnStatusChanged;
 193import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 194import eu.siacs.conversations.xmpp.XmppConnection;
 195import eu.siacs.conversations.xmpp.chatstate.ChatState;
 196import eu.siacs.conversations.xmpp.forms.Data;
 197import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
 198import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
 199import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 200import eu.siacs.conversations.xmpp.jingle.Media;
 201import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
 202import eu.siacs.conversations.xmpp.mam.MamReference;
 203import eu.siacs.conversations.xmpp.pep.Avatar;
 204import eu.siacs.conversations.xmpp.pep.PublishOptions;
 205import im.conversations.android.xmpp.model.stanza.Iq;
 206import java.io.File;
 207import java.security.Security;
 208import java.security.cert.CertificateException;
 209import java.security.cert.X509Certificate;
 210import java.util.ArrayList;
 211import java.util.Arrays;
 212import java.util.Collection;
 213import java.util.Collections;
 214import java.util.HashSet;
 215import java.util.Iterator;
 216import java.util.List;
 217import java.util.ListIterator;
 218import java.util.Map;
 219import java.util.Set;
 220import java.util.WeakHashMap;
 221import java.util.concurrent.CopyOnWriteArrayList;
 222import java.util.concurrent.CountDownLatch;
 223import java.util.concurrent.Executor;
 224import java.util.concurrent.Executors;
 225import java.util.concurrent.RejectedExecutionException;
 226import java.util.concurrent.ScheduledExecutorService;
 227import java.util.concurrent.TimeUnit;
 228import java.util.concurrent.atomic.AtomicBoolean;
 229import java.util.concurrent.atomic.AtomicLong;
 230import java.util.concurrent.atomic.AtomicReference;
 231import java.util.function.Consumer;
 232import me.leolin.shortcutbadger.ShortcutBadger;
 233import org.conscrypt.Conscrypt;
 234import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep;
 235import org.openintents.openpgp.IOpenPgpService2;
 236import org.openintents.openpgp.util.OpenPgpApi;
 237import org.openintents.openpgp.util.OpenPgpServiceConnection;
 238
 239import okhttp3.HttpUrl;
 240import okhttp3.OkHttpClient;
 241
 242public class XmppConnectionService extends Service {
 243
 244    public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations";
 245    public static final String ACTION_MARK_AS_READ = "mark_as_read";
 246    public static final String ACTION_SNOOZE = "snooze";
 247    public static final String ACTION_CLEAR_MESSAGE_NOTIFICATION = "clear_message_notification";
 248    public static final String ACTION_CLEAR_MISSED_CALL_NOTIFICATION =
 249            "clear_missed_call_notification";
 250    public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error";
 251    public static final String ACTION_TRY_AGAIN = "try_again";
 252
 253    public static final String ACTION_TEMPORARILY_DISABLE = "temporarily_disable";
 254    public static final String ACTION_PING = "ping";
 255    public static final String ACTION_IDLE_PING = "idle_ping";
 256    public static final String ACTION_INTERNAL_PING = "internal_ping";
 257    public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh";
 258    public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received";
 259    public static final String ACTION_DISMISS_CALL = "dismiss_call";
 260    public static final String ACTION_END_CALL = "end_call";
 261    public static final String ACTION_STARTING_CALL = "starting_call";
 262    public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
 263    public static final String ACTION_CALL_INTEGRATION_SERVICE_STARTED =
 264            "call_integration_service_started";
 265    private static final String ACTION_POST_CONNECTIVITY_CHANGE =
 266            "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
 267    public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS =
 268            "eu.siacs.conversations.UNIFIED_PUSH_RENEW";
 269    public static final String ACTION_QUICK_LOG = "eu.siacs.conversations.QUICK_LOG";
 270
 271    private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
 272
 273    public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
 274    private static final Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor();
 275    public static final Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor();
 276
 277    private final ScheduledExecutorService internalPingExecutor =
 278            Executors.newSingleThreadScheduledExecutor();
 279    private static final SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR =
 280            new SerialSingleThreadExecutor("VideoCompression");
 281    private final SerialSingleThreadExecutor mDatabaseWriterExecutor =
 282            new SerialSingleThreadExecutor("DatabaseWriter");
 283    private final SerialSingleThreadExecutor mDatabaseReaderExecutor =
 284            new SerialSingleThreadExecutor("DatabaseReader");
 285    private final SerialSingleThreadExecutor mNotificationExecutor =
 286            new SerialSingleThreadExecutor("NotificationExecutor");
 287    private final ReplacingTaskManager mRosterSyncTaskManager = new ReplacingTaskManager();
 288    private final IBinder mBinder = new XmppConnectionBinder();
 289    private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
 290    private final IqGenerator mIqGenerator = new IqGenerator(this);
 291    private final Set<String> mInProgressAvatarFetches = new HashSet<>();
 292    private final Set<String> mOmittedPepAvatarFetches = new HashSet<>();
 293    private final HashSet<Jid> mLowPingTimeoutMode = new HashSet<>();
 294    private final Consumer<Iq> mDefaultIqHandler =
 295            (packet) -> {
 296                if (packet.getType() != Iq.Type.RESULT) {
 297                    final var error = packet.getError();
 298                    String text = error != null ? error.findChildContent("text") : null;
 299                    if (text != null) {
 300                        Log.d(Config.LOGTAG, "received iq error: " + text);
 301                    }
 302                }
 303            };
 304    public DatabaseBackend databaseBackend;
 305    private Multimap<String, String> mutedMucUsers;
 306    private final ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger");
 307    private final ReplacingSerialSingleThreadExecutor mStickerScanExecutor = new ReplacingSerialSingleThreadExecutor("StickerScan");
 308    private long mLastActivity = 0;
 309    private long mLastMucPing = 0;
 310    private Map<String, Message> mScheduledMessages = new HashMap<>();
 311    private long mLastStickerRescan = 0;
 312    private final AppSettings appSettings = new AppSettings(this);
 313    private final FileBackend fileBackend = new FileBackend(this);
 314    private MemorizingTrustManager mMemorizingTrustManager;
 315    private final NotificationService mNotificationService = new NotificationService(this);
 316    private final UnifiedPushBroker unifiedPushBroker = new UnifiedPushBroker(this);
 317    private final ChannelDiscoveryService mChannelDiscoveryService =
 318            new ChannelDiscoveryService(this);
 319    private final ShortcutService mShortcutService = new ShortcutService(this);
 320    private final AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false);
 321    private final AtomicBoolean mOngoingVideoTranscoding = new AtomicBoolean(false);
 322    private final AtomicBoolean mForceDuringOnCreate = new AtomicBoolean(false);
 323    private final AtomicReference<OngoingCall> ongoingCall = new AtomicReference<>();
 324    private final MessageGenerator mMessageGenerator = new MessageGenerator(this);
 325    public OnContactStatusChanged onContactStatusChanged =
 326            (contact, online) -> {
 327                final var conversation = find(contact);
 328                if (conversation == null) {
 329                    return;
 330                }
 331                if (online) {
 332                    if (contact.getPresences().size() == 1) {
 333                        sendUnsentMessages(conversation);
 334                    }
 335                }
 336            };
 337    private final PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
 338    private List<Account> accounts;
 339    private final JingleConnectionManager mJingleConnectionManager =
 340            new JingleConnectionManager(this);
 341    private final HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(this);
 342    private final AvatarService mAvatarService = new AvatarService(this);
 343    private final MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
 344    private final PushManagementService mPushManagementService = new PushManagementService(this);
 345    private final QuickConversationsService mQuickConversationsService =
 346            new QuickConversationsService(this);
 347    private final ConversationsFileObserver fileObserver =
 348            new ConversationsFileObserver(
 349                    Environment.getExternalStorageDirectory().getAbsolutePath()) {
 350                @Override
 351                public void onEvent(final int event, final File file) {
 352                    markFileDeleted(file);
 353                }
 354            };
 355    private final OnMessageAcknowledged mOnMessageAcknowledgedListener =
 356            new OnMessageAcknowledged() {
 357
 358                @Override
 359                public boolean onMessageAcknowledged(
 360                        final Account account, final Jid to, final String id) {
 361                    if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
 362                        final String sessionId =
 363                                id.substring(
 364                                        JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX
 365                                                .length());
 366                        mJingleConnectionManager.updateProposedSessionDiscovered(
 367                                account,
 368                                to,
 369                                sessionId,
 370                                JingleConnectionManager.DeviceDiscoveryState
 371                                        .SEARCHING_ACKNOWLEDGED);
 372                    }
 373
 374                    final Jid bare = to.asBareJid();
 375
 376                    for (final Conversation conversation : getConversations()) {
 377                        if (conversation.getAccount() == account
 378                                && conversation.getJid().asBareJid().equals(bare)) {
 379                            final Message message = conversation.findUnsentMessageWithUuid(id);
 380                            if (message != null) {
 381                                message.setStatus(Message.STATUS_SEND);
 382                                message.setErrorMessage(null);
 383                                databaseBackend.updateMessage(message, false);
 384                                return true;
 385                            }
 386                        }
 387                    }
 388                    return false;
 389                }
 390            };
 391
 392    private final AtomicBoolean diallerIntegrationActive = new AtomicBoolean(false);
 393
 394    public void setDiallerIntegrationActive(boolean active) {
 395      diallerIntegrationActive.set(active);
 396    }
 397
 398    private boolean destroyed = false;
 399
 400    private int unreadCount = -1;
 401
 402    // Ui callback listeners
 403    private final Set<OnConversationUpdate> mOnConversationUpdates =
 404            Collections.newSetFromMap(new WeakHashMap<OnConversationUpdate, Boolean>());
 405    private final Set<OnShowErrorToast> mOnShowErrorToasts =
 406            Collections.newSetFromMap(new WeakHashMap<OnShowErrorToast, Boolean>());
 407    private final Set<OnAccountUpdate> mOnAccountUpdates =
 408            Collections.newSetFromMap(new WeakHashMap<OnAccountUpdate, Boolean>());
 409    private final Set<OnCaptchaRequested> mOnCaptchaRequested =
 410            Collections.newSetFromMap(new WeakHashMap<OnCaptchaRequested, Boolean>());
 411    private final Set<OnRosterUpdate> mOnRosterUpdates =
 412            Collections.newSetFromMap(new WeakHashMap<OnRosterUpdate, Boolean>());
 413    private final Set<OnUpdateBlocklist> mOnUpdateBlocklist =
 414            Collections.newSetFromMap(new WeakHashMap<OnUpdateBlocklist, Boolean>());
 415    private final Set<OnMucRosterUpdate> mOnMucRosterUpdate =
 416            Collections.newSetFromMap(new WeakHashMap<OnMucRosterUpdate, Boolean>());
 417    private final Set<OnKeyStatusUpdated> mOnKeyStatusUpdated =
 418            Collections.newSetFromMap(new WeakHashMap<OnKeyStatusUpdated, Boolean>());
 419    private final Set<OnJingleRtpConnectionUpdate> onJingleRtpConnectionUpdate =
 420            Collections.newSetFromMap(new WeakHashMap<OnJingleRtpConnectionUpdate, Boolean>());
 421
 422    private final Object LISTENER_LOCK = new Object();
 423
 424    public final Set<String> FILENAMES_TO_IGNORE_DELETION = new HashSet<>();
 425
 426    private final AtomicLong mLastExpiryRun = new AtomicLong(0);
 427    private final LruCache<Pair<String, String>, ServiceDiscoveryResult> discoCache =
 428            new LruCache<>(20);
 429    private final OnStatusChanged statusListener =
 430            new OnStatusChanged() {
 431
 432                @Override
 433                public void onStatusChanged(final Account account) {
 434                    XmppConnection connection = account.getXmppConnection();
 435                    updateAccountUi();
 436
 437                    if (account.getStatus() == Account.State.ONLINE
 438                            || account.getStatus().isError()) {
 439                        mQuickConversationsService.signalAccountStateChange();
 440                    }
 441
 442                    if (account.getStatus() == Account.State.ONLINE) {
 443                        synchronized (mLowPingTimeoutMode) {
 444                            if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
 445                                Log.d(
 446                                        Config.LOGTAG,
 447                                        account.getJid().asBareJid()
 448                                                + ": leaving low ping timeout mode");
 449                            }
 450                        }
 451                        if (account.setShowErrorNotification(true)) {
 452                            databaseBackend.updateAccount(account);
 453                        }
 454                        mMessageArchiveService.executePendingQueries(account);
 455                        if (connection != null && connection.getFeatures().csi()) {
 456                            if (checkListeners()) {
 457                                Log.d(
 458                                        Config.LOGTAG,
 459                                        account.getJid().asBareJid() + " sending csi//inactive");
 460                                connection.sendInactive();
 461                            } else {
 462                                Log.d(
 463                                        Config.LOGTAG,
 464                                        account.getJid().asBareJid() + " sending csi//active");
 465                                connection.sendActive();
 466                            }
 467                        }
 468                        List<Conversation> conversations = getConversations();
 469                        for (Conversation conversation : conversations) {
 470                            final boolean inProgressJoin;
 471                            synchronized (account.inProgressConferenceJoins) {
 472                                inProgressJoin =
 473                                        account.inProgressConferenceJoins.contains(conversation);
 474                            }
 475                            final boolean pendingJoin;
 476                            synchronized (account.pendingConferenceJoins) {
 477                                pendingJoin = account.pendingConferenceJoins.contains(conversation);
 478                            }
 479                            if (conversation.getAccount() == account
 480                                    && !pendingJoin
 481                                    && !inProgressJoin) {
 482                                sendUnsentMessages(conversation);
 483                            }
 484                        }
 485                        final List<Conversation> pendingLeaves;
 486                        synchronized (account.pendingConferenceLeaves) {
 487                            pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves);
 488                            account.pendingConferenceLeaves.clear();
 489                        }
 490                        for (Conversation conversation : pendingLeaves) {
 491                            leaveMuc(conversation);
 492                        }
 493                        final List<Conversation> pendingJoins;
 494                        synchronized (account.pendingConferenceJoins) {
 495                            pendingJoins = new ArrayList<>(account.pendingConferenceJoins);
 496                            account.pendingConferenceJoins.clear();
 497                        }
 498                        for (Conversation conversation : pendingJoins) {
 499                            joinMuc(conversation);
 500                        }
 501                        scheduleWakeUpCall(
 502                                Config.PING_MAX_INTERVAL * 1000L, account.getUuid().hashCode());
 503                    } else if (account.getStatus() == Account.State.OFFLINE
 504                            || account.getStatus() == Account.State.DISABLED
 505                            || account.getStatus() == Account.State.LOGGED_OUT) {
 506                        resetSendingToWaiting(account);
 507                        if (account.isConnectionEnabled() && isInLowPingTimeoutMode(account)) {
 508                            Log.d(
 509                                    Config.LOGTAG,
 510                                    account.getJid().asBareJid()
 511                                            + ": went into offline state during low ping mode."
 512                                            + " reconnecting now");
 513                            reconnectAccount(account, true, false);
 514                        } else {
 515                            final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2;
 516                            scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
 517                        }
 518                    } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
 519                        databaseBackend.updateAccount(account);
 520                        reconnectAccount(account, true, false);
 521                    } else if (account.getStatus() != Account.State.CONNECTING
 522                            && account.getStatus() != Account.State.NO_INTERNET) {
 523                        resetSendingToWaiting(account);
 524                        if (connection != null && account.getStatus().isAttemptReconnect()) {
 525                            final boolean aggressive =
 526                                    account.getStatus() == Account.State.SEE_OTHER_HOST
 527                                            || hasJingleRtpConnection(account);
 528                            final int next = connection.getTimeToNextAttempt(aggressive);
 529                            final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account);
 530                            if (next <= 0) {
 531                                Log.d(
 532                                        Config.LOGTAG,
 533                                        account.getJid().asBareJid()
 534                                                + ": error connecting account. reconnecting now."
 535                                                + " lowPingTimeout="
 536                                                + lowPingTimeoutMode);
 537                                reconnectAccount(account, true, false);
 538                            } else {
 539                                final int attempt = connection.getAttempt() + 1;
 540                                Log.d(
 541                                        Config.LOGTAG,
 542                                        account.getJid().asBareJid()
 543                                                + ": error connecting account. try again in "
 544                                                + next
 545                                                + "s for the "
 546                                                + attempt
 547                                                + " time. lowPingTimeout="
 548                                                + lowPingTimeoutMode
 549                                                + ", aggressive="
 550                                                + aggressive);
 551                                scheduleWakeUpCall(next, account.getUuid().hashCode());
 552                                if (aggressive) {
 553                                    internalPingExecutor.schedule(
 554                                            XmppConnectionService.this
 555                                                    ::manageAccountConnectionStatesInternal,
 556                                            (next * 1000L) + 50,
 557                                            TimeUnit.MILLISECONDS);
 558                                }
 559                            }
 560                        }
 561                    }
 562                    getNotificationService().updateErrorNotification();
 563                }
 564            };
 565    private OpenPgpServiceConnection pgpServiceConnection;
 566    private PgpEngine mPgpEngine = null;
 567    private WakeLock wakeLock;
 568    private LruCache<String, Drawable> mDrawableCache;
 569    private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
 570    private final BroadcastReceiver mInternalRestrictedEventReceiver =
 571            new RestrictedEventReceiver(Arrays.asList(TorServiceUtils.ACTION_STATUS));
 572    private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
 573    private EmojiSearch emojiSearch = null;
 574
 575    private static String generateFetchKey(Account account, final Avatar avatar) {
 576        return account.getJid().asBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum;
 577    }
 578
 579    private boolean isInLowPingTimeoutMode(Account account) {
 580        synchronized (mLowPingTimeoutMode) {
 581            return mLowPingTimeoutMode.contains(account.getJid().asBareJid());
 582        }
 583    }
 584
 585    public void startOngoingVideoTranscodingForegroundNotification() {
 586        mOngoingVideoTranscoding.set(true);
 587        toggleForegroundService();
 588    }
 589
 590    public void stopOngoingVideoTranscodingForegroundNotification() {
 591        mOngoingVideoTranscoding.set(false);
 592        toggleForegroundService();
 593    }
 594
 595    public boolean areMessagesInitialized() {
 596        return this.restoredFromDatabaseLatch.getCount() == 0;
 597    }
 598
 599    public PgpEngine getPgpEngine() {
 600        if (!Config.supportOpenPgp()) {
 601            return null;
 602        } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
 603            if (this.mPgpEngine == null) {
 604                this.mPgpEngine =
 605                        new PgpEngine(
 606                                new OpenPgpApi(
 607                                        getApplicationContext(), pgpServiceConnection.getService()),
 608                                this);
 609            }
 610            return mPgpEngine;
 611        } else {
 612            return null;
 613        }
 614    }
 615
 616    public OpenPgpApi getOpenPgpApi() {
 617        if (!Config.supportOpenPgp()) {
 618            return null;
 619        } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
 620            return new OpenPgpApi(this, pgpServiceConnection.getService());
 621        } else {
 622            return null;
 623        }
 624    }
 625
 626    public AppSettings getAppSettings() {
 627        return this.appSettings;
 628    }
 629
 630    public FileBackend getFileBackend() {
 631        return this.fileBackend;
 632    }
 633
 634    public DownloadableFile getFileForCid(Cid cid) {
 635        return this.databaseBackend.getFileForCid(cid);
 636    }
 637
 638    public String getUrlForCid(Cid cid) {
 639        return this.databaseBackend.getUrlForCid(cid);
 640    }
 641
 642    public void saveCid(Cid cid, File file) throws BlockedMediaException {
 643        saveCid(cid, file, null);
 644    }
 645
 646    public void saveCid(Cid cid, File file, String url) throws BlockedMediaException {
 647        if (this.databaseBackend.isBlockedMedia(cid)) {
 648            throw new BlockedMediaException();
 649        }
 650        this.databaseBackend.saveCid(cid, file, url);
 651    }
 652
 653    public boolean muteMucUser(MucOptions.User user) {
 654        boolean muted = databaseBackend.muteMucUser(user);
 655        if (!muted) return false;
 656        mutedMucUsers.put(user.getMuc().toString(), user.getOccupantId());
 657        return true;
 658    }
 659
 660    public boolean unmuteMucUser(MucOptions.User user) {
 661        boolean unmuted = databaseBackend.unmuteMucUser(user);
 662        if (!unmuted) return false;
 663        mutedMucUsers.remove(user.getMuc().toString(), user.getOccupantId());
 664        return true;
 665    }
 666
 667    public boolean isMucUserMuted(MucOptions.User user) {
 668        return mutedMucUsers.containsEntry("" + user.getMuc(), user.getOccupantId());
 669    }
 670
 671    public void blockMedia(File f) {
 672        try {
 673            Cid[] cids = getFileBackend().calculateCids(new FileInputStream(f));
 674            for (Cid cid : cids) {
 675                blockMedia(cid);
 676            }
 677        } catch (final IOException e) { }
 678    }
 679
 680    public void blockMedia(Cid cid) {
 681        this.databaseBackend.blockMedia(cid);
 682    }
 683
 684    public void clearBlockedMedia() {
 685        this.databaseBackend.clearBlockedMedia();
 686    }
 687
 688    public Message getMessage(Conversation conversation, String uuid) {
 689        return this.databaseBackend.getMessage(conversation, uuid);
 690    }
 691
 692    public Map<String, Message> getMessageFuzzyIds(Conversation conversation, Collection<String> ids) {
 693        return this.databaseBackend.getMessageFuzzyIds(conversation, ids);
 694    }
 695
 696    public void insertWebxdcUpdate(final WebxdcUpdate update) {
 697        this.databaseBackend.insertWebxdcUpdate(update);
 698    }
 699
 700    public WebxdcUpdate findLastWebxdcUpdate(Message message) {
 701        return this.databaseBackend.findLastWebxdcUpdate(message);
 702    }
 703
 704    public List<WebxdcUpdate> findWebxdcUpdates(Message message, long serial) {
 705        return this.databaseBackend.findWebxdcUpdates(message, serial);
 706    }
 707
 708    public AvatarService getAvatarService() {
 709        return this.mAvatarService;
 710    }
 711
 712    public void attachLocationToConversation(
 713            final Conversation conversation, final Uri uri, final String subject, final UiCallback<Message> callback) {
 714        int encryption = conversation.getNextEncryption();
 715        if (encryption == Message.ENCRYPTION_PGP) {
 716            encryption = Message.ENCRYPTION_DECRYPTED;
 717        }
 718        Message message = new Message(conversation, uri.toString(), encryption);
 719        if (subject != null && subject.length() > 0) message.setSubject(subject);
 720        message.setThread(conversation.getThread());
 721        Message.configurePrivateMessage(message);
 722        if (encryption == Message.ENCRYPTION_DECRYPTED) {
 723            getPgpEngine().encrypt(message, callback);
 724        } else {
 725            sendMessage(message);
 726            callback.success(message);
 727        }
 728    }
 729
 730    public void attachFileToConversation(
 731            final Conversation conversation,
 732            final Uri uri,
 733            final String type,
 734            final String subject,
 735            final UiCallback<Message> callback) {
 736        final Message message;
 737        if (conversation.getReplyTo() == null) {
 738            message = new Message(conversation, "", conversation.getNextEncryption());
 739        } else {
 740            message = conversation.getReplyTo().reply();
 741            message.setEncryption(conversation.getNextEncryption());
 742        }
 743        if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 744            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 745        }
 746        if (subject != null && subject.length() > 0) message.setSubject(subject);
 747        message.setThread(conversation.getThread());
 748        if (!Message.configurePrivateFileMessage(message)) {
 749            message.setCounterpart(conversation.getNextCounterpart());
 750            message.setType(Message.TYPE_FILE);
 751        }
 752        Log.d(Config.LOGTAG, "attachFile: type=" + message.getType());
 753        Log.d(Config.LOGTAG, "counterpart=" + message.getCounterpart());
 754        final AttachFileToConversationRunnable runnable =
 755                new AttachFileToConversationRunnable(this, uri, type, message, callback);
 756        if (runnable.isVideoMessage()) {
 757            VIDEO_COMPRESSION_EXECUTOR.execute(runnable);
 758        } else {
 759            FILE_ATTACHMENT_EXECUTOR.execute(runnable);
 760        }
 761    }
 762
 763    public void attachImageToConversation(
 764            final Conversation conversation,
 765            final Uri uri,
 766            final String type,
 767            final String subject,
 768            final UiCallback<Message> callback) {
 769        final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type);
 770        final String compressPictures = getCompressPicturesPreference();
 771
 772        if ("never".equals(compressPictures)
 773                || ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))
 774                || (mimeType != null && mimeType.endsWith("/gif"))
 775                || getFileBackend().unusualBounds(uri) || "data".equals(uri.getScheme())) {
 776            Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": not compressing picture. sending as file");
 777            attachFileToConversation(conversation, uri, mimeType, subject, callback);
 778            return;
 779        }
 780        final Message message;
 781
 782        if (conversation.getReplyTo() == null) {
 783            message = new Message(conversation, "", conversation.getNextEncryption());
 784        } else {
 785            message = conversation.getReplyTo().reply();
 786            message.setEncryption(conversation.getNextEncryption());
 787        }
 788        if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 789            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 790        }
 791        if (subject != null && subject.length() > 0) message.setSubject(subject);
 792        message.setThread(conversation.getThread());
 793        if (!Message.configurePrivateFileMessage(message)) {
 794            message.setCounterpart(conversation.getNextCounterpart());
 795            message.setType(Message.TYPE_IMAGE);
 796        }
 797        Log.d(Config.LOGTAG, "attachImage: type=" + message.getType());
 798        FILE_ATTACHMENT_EXECUTOR.execute(() -> {
 799            try {
 800                getFileBackend().copyImageToPrivateStorage(message, uri);
 801            } catch (FileBackend.ImageCompressionException e) {
 802                Log.d(Config.LOGTAG, "unable to compress image. fall back to file transfer", e);
 803                attachFileToConversation(conversation, uri, mimeType, subject, callback);
 804                return;
 805            } catch (final FileBackend.FileCopyException e) {
 806                callback.error(e.getResId(), message);
 807                return;
 808            }
 809            if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 810                final PgpEngine pgpEngine = getPgpEngine();
 811                if (pgpEngine != null) {
 812                    pgpEngine.encrypt(message, callback);
 813                } else if (callback != null) {
 814                    callback.error(R.string.unable_to_connect_to_keychain, null);
 815                }
 816            } else {
 817                sendMessage(message, false, false, false, () -> callback.success(message));
 818            }
 819        });
 820    }
 821
 822    private File stickerDir() {
 823        SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
 824        final String dir = p.getString("sticker_directory", "Stickers");
 825        if (dir.startsWith("content://")) {
 826            Uri uri = Uri.parse(dir);
 827            uri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri));
 828            return new File(FileUtils.getPath(getBaseContext(), uri));
 829        } else {
 830            return new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/" + dir);
 831        }
 832    }
 833
 834    public void rescanStickers() {
 835        long msToRescan = (mLastStickerRescan + 600000L) - SystemClock.elapsedRealtime();
 836        if (msToRescan > 0) return;
 837        Log.d(Config.LOGTAG, "rescanStickers");
 838
 839        mLastStickerRescan = SystemClock.elapsedRealtime();
 840        mStickerScanExecutor.execute(() -> {
 841            Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
 842            try {
 843                for (File file : Files.fileTraverser().breadthFirst(stickerDir())) {
 844                    try {
 845                        if (file.isFile() && file.canRead()) {
 846                            DownloadableFile df = new DownloadableFile(file.getAbsolutePath());
 847                            Drawable icon = fileBackend.getThumbnail(df, getResources(), (int) (getResources().getDisplayMetrics().density * 288), false);
 848                            final String filename = Files.getNameWithoutExtension(df.getName());
 849                            Cid[] cids = fileBackend.calculateCids(new FileInputStream(df));
 850                            for (Cid cid : cids) {
 851                                saveCid(cid, file);
 852                            }
 853                            if (file.length() < 129000) {
 854                                emojiSearch.addEmoji(new EmojiSearch.CustomEmoji(filename, cids[0].toString(), icon, file.getParentFile().getName()));
 855                            }
 856                        }
 857                    } catch (final Exception e) {
 858                        Log.w(Config.LOGTAG, "rescanStickers: " + e);
 859                    }
 860                }
 861            } catch (final Exception e) {
 862                Log.w(Config.LOGTAG, "rescanStickers: " + e);
 863            }
 864        });
 865    }
 866
 867    protected void cleanupCache() {
 868        if (Build.VERSION.SDK_INT < 26) return; // Doesn't support file.toPath
 869        mStickerScanExecutor.execute(() -> {
 870            Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
 871            final var now = System.currentTimeMillis();
 872            try {
 873                for (File file : Files.fileTraverser().breadthFirst(getCacheDir())) {
 874                    if (file.isFile() && file.canRead() && file.canWrite()) {
 875                        final var attrs = java.nio.file.Files.readAttributes(file.toPath(), java.nio.file.attribute.BasicFileAttributes.class);
 876                        if ((now - attrs.lastAccessTime().toMillis()) > 1000L * 60 * 60 * 24 * 10) {
 877                            Log.d(Config.LOGTAG, "cleanupCache removing file not used recently: " + file);
 878                            file.delete();
 879                        }
 880                    }
 881                }
 882            } catch (final Exception e) {
 883                Log.w(Config.LOGTAG, "cleanupCache " + e);
 884            }
 885        });
 886    }
 887
 888    public EmojiSearch emojiSearch() {
 889        return emojiSearch;
 890    }
 891
 892    public Conversation find(Bookmark bookmark) {
 893        return find(bookmark.getAccount(), bookmark.getJid());
 894    }
 895
 896    public Conversation find(final Account account, final Jid jid) {
 897        return find(getConversations(), account, jid);
 898    }
 899
 900    public boolean isMuc(final Account account, final Jid jid) {
 901        final Conversation c = find(account, jid);
 902        return c != null && c.getMode() == Conversational.MODE_MULTI;
 903    }
 904
 905    public void search(
 906            final List<String> term,
 907            final String uuid,
 908            final OnSearchResultsAvailable onSearchResultsAvailable) {
 909        MessageSearchTask.search(this, term, uuid, onSearchResultsAvailable);
 910    }
 911
 912    @Override
 913    public int onStartCommand(final Intent intent, int flags, int startId) {
 914        final var nomedia = getBooleanPreference("nomedia", R.bool.default_nomedia);
 915        fileBackend.setupNomedia(nomedia);
 916        final String action = Strings.nullToEmpty(intent == null ? null : intent.getAction());
 917        final boolean needsForegroundService =
 918                intent != null
 919                        && intent.getBooleanExtra(
 920                                SystemEventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false);
 921        if (needsForegroundService) {
 922            Log.d(
 923                    Config.LOGTAG,
 924                    "toggle forced foreground service after receiving event (action="
 925                            + action
 926                            + ")");
 927            toggleForegroundService(true, action.equals(ACTION_STARTING_CALL));
 928        }
 929        final String uuid = intent == null ? null : intent.getStringExtra("uuid");
 930        switch (action) {
 931            case QuickConversationsService.SMS_RETRIEVED_ACTION:
 932                mQuickConversationsService.handleSmsReceived(intent);
 933                break;
 934            case ConnectivityManager.CONNECTIVITY_ACTION:
 935                if (hasInternetConnection()) {
 936                    if (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0) {
 937                        schedulePostConnectivityChange();
 938                    }
 939                    if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
 940                        resetAllAttemptCounts(true, false);
 941                    }
 942                    Resolver.clearCache();
 943                }
 944                break;
 945            case Intent.ACTION_SHUTDOWN:
 946                logoutAndSave(true);
 947                return START_NOT_STICKY;
 948            case ACTION_CLEAR_MESSAGE_NOTIFICATION:
 949                mNotificationExecutor.execute(
 950                        () -> {
 951                            try {
 952                                final Conversation c = findConversationByUuid(uuid);
 953                                if (c != null) {
 954                                    mNotificationService.clearMessages(c);
 955                                } else {
 956                                    mNotificationService.clearMessages();
 957                                }
 958                                restoredFromDatabaseLatch.await();
 959
 960                            } catch (InterruptedException e) {
 961                                Log.d(
 962                                        Config.LOGTAG,
 963                                        "unable to process clear message notification");
 964                            }
 965                        });
 966                break;
 967            case ACTION_CLEAR_MISSED_CALL_NOTIFICATION:
 968                mNotificationExecutor.execute(
 969                        () -> {
 970                            try {
 971                                final Conversation c = findConversationByUuid(uuid);
 972                                if (c != null) {
 973                                    mNotificationService.clearMissedCalls(c);
 974                                } else {
 975                                    mNotificationService.clearMissedCalls();
 976                                }
 977                                restoredFromDatabaseLatch.await();
 978
 979                            } catch (InterruptedException e) {
 980                                Log.d(
 981                                        Config.LOGTAG,
 982                                        "unable to process clear missed call notification");
 983                            }
 984                        });
 985                break;
 986            case ACTION_DISMISS_CALL:
 987                {
 988                    if (intent == null) {
 989                        break;
 990                    }
 991                    final String sessionId =
 992                            intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
 993                    Log.d(
 994                            Config.LOGTAG,
 995                            "received intent to dismiss call with session id " + sessionId);
 996                    mJingleConnectionManager.rejectRtpSession(sessionId);
 997                    break;
 998                }
 999            case TorServiceUtils.ACTION_STATUS:
1000                final String status =
1001                        intent == null ? null : intent.getStringExtra(TorServiceUtils.EXTRA_STATUS);
1002                // TODO port and host are in 'extras' - but this may not be a reliable source?
1003                if ("ON".equals(status)) {
1004                    handleOrbotStartedEvent();
1005                    return START_STICKY;
1006                }
1007                break;
1008            case ACTION_END_CALL:
1009                {
1010                    if (intent == null) {
1011                        break;
1012                    }
1013                    final String sessionId =
1014                            intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
1015                    Log.d(
1016                            Config.LOGTAG,
1017                            "received intent to end call with session id " + sessionId);
1018                    mJingleConnectionManager.endRtpSession(sessionId);
1019                }
1020                break;
1021            case ACTION_PROVISION_ACCOUNT:
1022                {
1023                    if (intent == null) {
1024                        break;
1025                    }
1026                    final String address = intent.getStringExtra("address");
1027                    final String password = intent.getStringExtra("password");
1028                    if (QuickConversationsService.isQuicksy()
1029                            || Strings.isNullOrEmpty(address)
1030                            || Strings.isNullOrEmpty(password)) {
1031                        break;
1032                    }
1033                    provisionAccount(address, password);
1034                    break;
1035                }
1036            case ACTION_DISMISS_ERROR_NOTIFICATIONS:
1037                dismissErrorNotifications();
1038                break;
1039            case ACTION_TRY_AGAIN:
1040                resetAllAttemptCounts(false, true);
1041                break;
1042            case ACTION_REPLY_TO_CONVERSATION:
1043                final Bundle remoteInput =
1044                        intent == null ? null : RemoteInput.getResultsFromIntent(intent);
1045                if (remoteInput == null) {
1046                    break;
1047                }
1048                final CharSequence body = remoteInput.getCharSequence("text_reply");
1049                final boolean dismissNotification =
1050                        intent.getBooleanExtra("dismiss_notification", false);
1051                final String lastMessageUuid = intent.getStringExtra("last_message_uuid");
1052                if (body == null || body.length() <= 0) {
1053                    break;
1054                }
1055                mNotificationExecutor.execute(
1056                        () -> {
1057                            try {
1058                                restoredFromDatabaseLatch.await();
1059                                final Conversation c = findConversationByUuid(uuid);
1060                                if (c != null) {
1061                                    directReply(
1062                                            c,
1063                                            body.toString(),
1064                                            lastMessageUuid,
1065                                            dismissNotification);
1066                                }
1067                            } catch (InterruptedException e) {
1068                                Log.d(Config.LOGTAG, "unable to process direct reply");
1069                            }
1070                        });
1071                break;
1072            case ACTION_MARK_AS_READ:
1073                mNotificationExecutor.execute(
1074                        () -> {
1075                            final Conversation c = findConversationByUuid(uuid);
1076                            if (c == null) {
1077                                Log.d(
1078                                        Config.LOGTAG,
1079                                        "received mark read intent for unknown conversation ("
1080                                                + uuid
1081                                                + ")");
1082                                return;
1083                            }
1084                            try {
1085                                restoredFromDatabaseLatch.await();
1086                                sendReadMarker(c, null);
1087                            } catch (InterruptedException e) {
1088                                Log.d(
1089                                        Config.LOGTAG,
1090                                        "unable to process notification read marker for"
1091                                                + " conversation "
1092                                                + c.getName());
1093                            }
1094                        });
1095                break;
1096            case ACTION_SNOOZE:
1097                mNotificationExecutor.execute(
1098                        () -> {
1099                            final Conversation c = findConversationByUuid(uuid);
1100                            if (c == null) {
1101                                Log.d(
1102                                        Config.LOGTAG,
1103                                        "received snooze intent for unknown conversation ("
1104                                                + uuid
1105                                                + ")");
1106                                return;
1107                            }
1108                            c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000);
1109                            mNotificationService.clearMessages(c);
1110                            updateConversation(c);
1111                        });
1112            case AudioManager.RINGER_MODE_CHANGED_ACTION:
1113            case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED:
1114                if (dndOnSilentMode()) {
1115                    refreshAllPresences();
1116                }
1117                break;
1118            case Intent.ACTION_SCREEN_ON:
1119                deactivateGracePeriod();
1120            case Intent.ACTION_USER_PRESENT:
1121            case Intent.ACTION_SCREEN_OFF:
1122                if (awayWhenScreenLocked()) {
1123                    refreshAllPresences();
1124                }
1125                break;
1126            case ACTION_FCM_TOKEN_REFRESH:
1127                refreshAllFcmTokens();
1128                break;
1129            case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS:
1130                if (intent == null) {
1131                    break;
1132                }
1133                final String instance = intent.getStringExtra("instance");
1134                final String application = intent.getStringExtra("application");
1135                final Messenger messenger = intent.getParcelableExtra("messenger");
1136                final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger;
1137                if (messenger != null && application != null && instance != null) {
1138                    pushTargetMessenger =
1139                            new UnifiedPushBroker.PushTargetMessenger(
1140                                    new UnifiedPushDatabase.PushTarget(application, instance),
1141                                    messenger);
1142                    Log.d(Config.LOGTAG, "found push target messenger");
1143                } else {
1144                    pushTargetMessenger = null;
1145                }
1146                final Optional<UnifiedPushBroker.Transport> transport =
1147                        renewUnifiedPushEndpoints(pushTargetMessenger);
1148                if (instance != null && transport.isPresent()) {
1149                    unifiedPushBroker.rebroadcastEndpoint(messenger, instance, transport.get());
1150                }
1151                break;
1152            case ACTION_IDLE_PING:
1153                scheduleNextIdlePing();
1154                break;
1155            case ACTION_FCM_MESSAGE_RECEIVED:
1156                Log.d(Config.LOGTAG, "push message arrived in service. account");
1157                break;
1158            case ACTION_QUICK_LOG:
1159                final String message = intent == null ? null : intent.getStringExtra("message");
1160                if (message != null && Config.QUICK_LOG) {
1161                    quickLog(message);
1162                }
1163                break;
1164            case Intent.ACTION_SEND:
1165                final Uri uri = intent == null ? null : intent.getData();
1166                if (uri != null) {
1167                    Log.d(Config.LOGTAG, "received uri permission for " + uri);
1168                }
1169                return START_STICKY;
1170            case ACTION_TEMPORARILY_DISABLE:
1171                toggleSoftDisabled(true);
1172                if (checkListeners()) {
1173                    stopSelf();
1174                }
1175                return START_NOT_STICKY;
1176        }
1177        sendScheduledMessages();
1178        final var extras =  intent == null ? null : intent.getExtras();
1179        try {
1180            internalPingExecutor.execute(() -> manageAccountConnectionStates(action, extras));
1181        } catch (final RejectedExecutionException e) {
1182            Log.e(Config.LOGTAG, "can not schedule connection states manager");
1183        }
1184        if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) {
1185            expireOldMessages();
1186        }
1187        return START_STICKY;
1188    }
1189
1190    private void quickLog(final String message) {
1191        if (Strings.isNullOrEmpty(message)) {
1192            return;
1193        }
1194        final Account account = AccountUtils.getFirstEnabled(this);
1195        if (account == null) {
1196            return;
1197        }
1198        final Conversation conversation =
1199                findOrCreateConversation(account, Config.BUG_REPORTS, false, true);
1200        final Message report = new Message(conversation, message, Message.ENCRYPTION_NONE);
1201        report.setStatus(Message.STATUS_RECEIVED);
1202        conversation.add(report);
1203        databaseBackend.createMessage(report);
1204        updateConversationUi();
1205    }
1206
1207    private void manageAccountConnectionStatesInternal() {
1208        manageAccountConnectionStates(ACTION_INTERNAL_PING, null);
1209    }
1210
1211    private synchronized void manageAccountConnectionStates(
1212            final String action, final Bundle extras) {
1213        final String pushedAccountHash = extras == null ? null : extras.getString("account");
1214        final boolean interactive = java.util.Objects.equals(ACTION_TRY_AGAIN, action);
1215        WakeLockHelper.acquire(wakeLock);
1216        boolean pingNow =
1217                ConnectivityManager.CONNECTIVITY_ACTION.equals(action)
1218                        || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0
1219                                && ACTION_POST_CONNECTIVITY_CHANGE.equals(action));
1220        final HashSet<Account> pingCandidates = new HashSet<>();
1221        final String androidId = pushedAccountHash == null ? null : PhoneHelper.getAndroidId(this);
1222        for (final Account account : accounts) {
1223            final boolean pushWasMeantForThisAccount =
1224                    androidId != null
1225                            && CryptoHelper.getAccountFingerprint(account, androidId)
1226                                    .equals(pushedAccountHash);
1227            pingNow |=
1228                    processAccountState(
1229                            account,
1230                            interactive,
1231                            "ui".equals(action),
1232                            pushWasMeantForThisAccount,
1233                            pingCandidates);
1234        }
1235        if (pingNow) {
1236            for (final Account account : pingCandidates) {
1237                final var connection = account.getXmppConnection();
1238                final boolean lowTimeout = isInLowPingTimeoutMode(account);
1239                final var delta =
1240                        (SystemClock.elapsedRealtime() - connection.getLastPacketReceived())
1241                                / 1000L;
1242                connection.sendPing();
1243                Log.d(
1244                        Config.LOGTAG,
1245                        String.format(
1246                                "%s: send ping (action=%s,lowTimeout=%s,interval=%s)",
1247                                account.getJid().asBareJid(), action, lowTimeout, delta));
1248                scheduleWakeUpCall(
1249                        lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT,
1250                        account.getUuid().hashCode());
1251            }
1252        }
1253        long msToMucPing = (mLastMucPing + (Config.PING_MAX_INTERVAL * 2000L)) - SystemClock.elapsedRealtime();
1254        if (pingNow || ("ui".equals(action) && msToMucPing <= 0) || msToMucPing < -300000) {
1255            Log.d(Config.LOGTAG, "ping MUCs");
1256            mLastMucPing = SystemClock.elapsedRealtime();
1257            for (Conversation c : getConversations()) {
1258                if (c.getMode() == Conversation.MODE_MULTI && (c.getMucOptions().online() || c.getMucOptions().getError() == MucOptions.Error.SHUTDOWN)) {
1259                    mucSelfPingAndRejoin(c);
1260                }
1261            }
1262        }
1263        WakeLockHelper.release(wakeLock);
1264    }
1265
1266    private void sendScheduledMessages() {
1267        Log.d(Config.LOGTAG, "looking for and sending scheduled messages");
1268
1269        for (final var message : new ArrayList<>(mScheduledMessages.values())) {
1270            if (message.getTimeSent() > System.currentTimeMillis()) continue;
1271
1272            final var conversation = message.getConversation();
1273            final var account = conversation.getAccount();
1274            final boolean inProgressJoin;
1275            synchronized (account.inProgressConferenceJoins) {
1276                inProgressJoin = account.inProgressConferenceJoins.contains(conversation);
1277            }
1278            final boolean pendingJoin;
1279            synchronized (account.pendingConferenceJoins) {
1280                pendingJoin = account.pendingConferenceJoins.contains(conversation);
1281            }
1282            if (conversation.getAccount() == account
1283                    && !pendingJoin
1284                    && !inProgressJoin) {
1285                resendMessage(message, false);
1286            }
1287        }
1288    }
1289
1290    private void handleOrbotStartedEvent() {
1291        for (final Account account : accounts) {
1292            if (account.getStatus() == Account.State.TOR_NOT_AVAILABLE) {
1293                reconnectAccount(account, true, false);
1294            }
1295        }
1296    }
1297
1298    private boolean processAccountState(
1299            final Account account,
1300            final boolean interactive,
1301            final boolean isUiAction,
1302            final boolean isAccountPushed,
1303            final HashSet<Account> pingCandidates) {
1304        if (!account.getStatus().isAttemptReconnect()) {
1305            return false;
1306        }
1307        final var requestCode = account.getUuid().hashCode();
1308        if (!hasInternetConnection()) {
1309            account.setStatus(Account.State.NO_INTERNET);
1310            statusListener.onStatusChanged(account);
1311        } else {
1312            if (account.getStatus() == Account.State.NO_INTERNET) {
1313                account.setStatus(Account.State.OFFLINE);
1314                statusListener.onStatusChanged(account);
1315            }
1316            if (account.getStatus() == Account.State.ONLINE) {
1317                synchronized (mLowPingTimeoutMode) {
1318                    long lastReceived = account.getXmppConnection().getLastPacketReceived();
1319                    long lastSent = account.getXmppConnection().getLastPingSent();
1320                    long pingInterval =
1321                            isUiAction
1322                                    ? Config.PING_MIN_INTERVAL * 1000
1323                                    : Config.PING_MAX_INTERVAL * 1000;
1324                    long msToNextPing =
1325                            (Math.max(lastReceived, lastSent) + pingInterval)
1326                                    - SystemClock.elapsedRealtime();
1327                    int pingTimeout =
1328                            mLowPingTimeoutMode.contains(account.getJid().asBareJid())
1329                                    ? Config.LOW_PING_TIMEOUT * 1000
1330                                    : Config.PING_TIMEOUT * 1000;
1331                    long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
1332                    if (lastSent > lastReceived) {
1333                        if (pingTimeoutIn < 0) {
1334                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping timeout");
1335                            this.reconnectAccount(account, true, interactive);
1336                        } else {
1337                            this.scheduleWakeUpCall(pingTimeoutIn, requestCode);
1338                        }
1339                    } else {
1340                        pingCandidates.add(account);
1341                        if (isAccountPushed) {
1342                            if (mLowPingTimeoutMode.add(account.getJid().asBareJid())) {
1343                                Log.d(
1344                                        Config.LOGTAG,
1345                                        account.getJid().asBareJid()
1346                                                + ": entering low ping timeout mode");
1347                            }
1348                            return true;
1349                        } else if (msToNextPing <= 0) {
1350                            return true;
1351                        } else {
1352                            this.scheduleWakeUpCall(msToNextPing, requestCode);
1353                            if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
1354                                Log.d(
1355                                        Config.LOGTAG,
1356                                        account.getJid().asBareJid()
1357                                                + ": leaving low ping timeout mode");
1358                            }
1359                        }
1360                    }
1361                }
1362            } else if (account.getStatus() == Account.State.OFFLINE) {
1363                reconnectAccount(account, true, interactive);
1364            } else if (account.getStatus() == Account.State.CONNECTING) {
1365                final var connection = account.getXmppConnection();
1366                final var connectionDuration = connection.getConnectionDuration();
1367                final var discoDuration = connection.getDiscoDuration();
1368                final var connectionTimeout = Config.CONNECT_TIMEOUT * 1000L - connectionDuration;
1369                final var discoTimeout = Config.CONNECT_DISCO_TIMEOUT * 1000L - discoDuration;
1370                if (connectionTimeout < 0) {
1371                    connection.triggerConnectionTimeout();
1372                } else if (discoTimeout < 0) {
1373                    connection.sendDiscoTimeout();
1374                    scheduleWakeUpCall(discoTimeout, requestCode);
1375                } else {
1376                    scheduleWakeUpCall(Math.min(connectionTimeout, discoTimeout), requestCode);
1377                }
1378            } else {
1379                final boolean aggressive =
1380                        account.getStatus() == Account.State.SEE_OTHER_HOST
1381                                || hasJingleRtpConnection(account);
1382                if (account.getXmppConnection().getTimeToNextAttempt(aggressive) <= 0) {
1383                    reconnectAccount(account, true, interactive);
1384                }
1385            }
1386        }
1387        return false;
1388    }
1389
1390    private void toggleSoftDisabled(final boolean softDisabled) {
1391        for (final Account account : this.accounts) {
1392            if (account.isEnabled()) {
1393                if (account.setOption(Account.OPTION_SOFT_DISABLED, softDisabled)) {
1394                    updateAccount(account);
1395                }
1396            }
1397        }
1398    }
1399
1400    public boolean processUnifiedPushMessage(
1401            final Account account, final Jid transport, final Element push) {
1402        return unifiedPushBroker.processPushMessage(account, transport, push);
1403    }
1404
1405    public void reinitializeMuclumbusService() {
1406        mChannelDiscoveryService.initializeMuclumbusService();
1407    }
1408
1409    public void discoverChannels(
1410            String query,
1411            ChannelDiscoveryService.Method method,
1412            Map<Jid, Account> mucServices,
1413            ChannelDiscoveryService.OnChannelSearchResultsFound onChannelSearchResultsFound) {
1414        mChannelDiscoveryService.discover(
1415                Strings.nullToEmpty(query).trim(), method, mucServices, onChannelSearchResultsFound);
1416    }
1417
1418    public boolean isDataSaverDisabled() {
1419        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
1420            return true;
1421        }
1422        final ConnectivityManager connectivityManager = getSystemService(ConnectivityManager.class);
1423        return !Compatibility.isActiveNetworkMetered(connectivityManager)
1424                || Compatibility.getRestrictBackgroundStatus(connectivityManager)
1425                        == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
1426    }
1427
1428    private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {
1429        final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid);
1430        Message message = new Message(conversation, body, conversation.getNextEncryption());
1431        if (inReplyTo != null) {
1432            if (Emoticons.isEmoji(body.replaceAll("\\s", ""))) {
1433                final var aggregated = inReplyTo.getAggregatedReactions();
1434                final ImmutableSet.Builder<String> reactionBuilder = new ImmutableSet.Builder<>();
1435                reactionBuilder.addAll(aggregated.ourReactions);
1436                reactionBuilder.add(body.replaceAll("\\s", ""));
1437                sendReactions(inReplyTo, reactionBuilder.build());
1438                return;
1439            } else {
1440                message = inReplyTo.reply();
1441            }
1442            message.clearFallbacks("urn:xmpp:reply:0");
1443            message.setBody(body);
1444            message.setEncryption(conversation.getNextEncryption());
1445        }
1446        if (inReplyTo != null && inReplyTo.isPrivateMessage()) {
1447            Message.configurePrivateMessage(message, inReplyTo.getCounterpart());
1448        }
1449        message.markUnread();
1450        if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1451            getPgpEngine()
1452                    .encrypt(
1453                            message,
1454                            new UiCallback<Message>() {
1455                                @Override
1456                                public void success(Message message) {
1457                                    if (dismissAfterReply) {
1458                                        markRead((Conversation) message.getConversation(), true);
1459                                    } else {
1460                                        mNotificationService.pushFromDirectReply(message);
1461                                    }
1462                                }
1463
1464                                @Override
1465                                public void error(int errorCode, Message object) {}
1466
1467                                @Override
1468                                public void userInputRequired(PendingIntent pi, Message object) {}
1469                            });
1470        } else {
1471            sendMessage(message);
1472            if (dismissAfterReply) {
1473                markRead(conversation, true);
1474            } else {
1475                mNotificationService.pushFromDirectReply(message);
1476            }
1477        }
1478    }
1479
1480    private boolean dndOnSilentMode() {
1481        return getBooleanPreference(AppSettings.DND_ON_SILENT_MODE, R.bool.dnd_on_silent_mode);
1482    }
1483
1484    private boolean manuallyChangePresence() {
1485        return getBooleanPreference(
1486                AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
1487    }
1488
1489    private boolean treatVibrateAsSilent() {
1490        return getBooleanPreference(
1491                AppSettings.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent);
1492    }
1493
1494    private boolean awayWhenScreenLocked() {
1495        return getBooleanPreference(
1496                AppSettings.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off);
1497    }
1498
1499    private String getCompressPicturesPreference() {
1500        return getPreferences()
1501                .getString(
1502                        "picture_compression",
1503                        getResources().getString(R.string.picture_compression));
1504    }
1505
1506    private Presence.Status getTargetPresence() {
1507        if (dndOnSilentMode() && isPhoneSilenced()) {
1508            return Presence.Status.DND;
1509        } else if (awayWhenScreenLocked() && isScreenLocked()) {
1510            return Presence.Status.AWAY;
1511        } else {
1512            return Presence.Status.ONLINE;
1513        }
1514    }
1515
1516    public boolean isScreenLocked() {
1517        final KeyguardManager keyguardManager = getSystemService(KeyguardManager.class);
1518        final PowerManager powerManager = getSystemService(PowerManager.class);
1519        final boolean locked = keyguardManager != null && keyguardManager.isKeyguardLocked();
1520        final boolean interactive;
1521        try {
1522            interactive = powerManager != null && powerManager.isInteractive();
1523        } catch (final Exception e) {
1524            return false;
1525        }
1526        return locked || !interactive;
1527    }
1528
1529    private boolean isPhoneSilenced() {
1530        final NotificationManager notificationManager = getSystemService(NotificationManager.class);
1531        final int filter =
1532                notificationManager == null
1533                        ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN
1534                        : notificationManager.getCurrentInterruptionFilter();
1535        final boolean notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY;
1536        final AudioManager audioManager = getSystemService(AudioManager.class);
1537        final int ringerMode =
1538                audioManager == null
1539                        ? AudioManager.RINGER_MODE_NORMAL
1540                        : audioManager.getRingerMode();
1541        try {
1542            if (treatVibrateAsSilent()) {
1543                return notificationDnd || ringerMode != AudioManager.RINGER_MODE_NORMAL;
1544            } else {
1545                return notificationDnd || ringerMode == AudioManager.RINGER_MODE_SILENT;
1546            }
1547        } catch (final Throwable throwable) {
1548            Log.d(
1549                    Config.LOGTAG,
1550                    "platform bug in isPhoneSilenced (" + throwable.getMessage() + ")");
1551            return notificationDnd;
1552        }
1553    }
1554
1555    private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) {
1556        Log.d(Config.LOGTAG, "resetting all attempt counts");
1557        for (Account account : accounts) {
1558            if (account.hasErrorStatus() || reallyAll) {
1559                final XmppConnection connection = account.getXmppConnection();
1560                if (connection != null) {
1561                    connection.resetAttemptCount(retryImmediately);
1562                }
1563            }
1564            if (account.setShowErrorNotification(true)) {
1565                mDatabaseWriterExecutor.execute(() -> databaseBackend.updateAccount(account));
1566            }
1567        }
1568        mNotificationService.updateErrorNotification();
1569    }
1570
1571    private void dismissErrorNotifications() {
1572        for (final Account account : this.accounts) {
1573            if (account.hasErrorStatus()) {
1574                Log.d(
1575                        Config.LOGTAG,
1576                        account.getJid().asBareJid() + ": dismissing error notification");
1577                if (account.setShowErrorNotification(false)) {
1578                    mDatabaseWriterExecutor.execute(() -> databaseBackend.updateAccount(account));
1579                }
1580            }
1581        }
1582    }
1583
1584    private void expireOldMessages() {
1585        expireOldMessages(false);
1586    }
1587
1588    public void expireOldMessages(final boolean resetHasMessagesLeftOnServer) {
1589        mLastExpiryRun.set(SystemClock.elapsedRealtime());
1590        mDatabaseWriterExecutor.execute(
1591                () -> {
1592                    long timestamp = getAutomaticMessageDeletionDate();
1593                    if (timestamp > 0) {
1594                        databaseBackend.expireOldMessages(timestamp);
1595                        synchronized (XmppConnectionService.this.conversations) {
1596                            for (Conversation conversation :
1597                                    XmppConnectionService.this.conversations) {
1598                                conversation.expireOldMessages(timestamp);
1599                                if (resetHasMessagesLeftOnServer) {
1600                                    conversation.messagesLoaded.set(true);
1601                                    conversation.setHasMessagesLeftOnServer(true);
1602                                }
1603                            }
1604                        }
1605                        updateConversationUi();
1606                    }
1607                });
1608    }
1609
1610    public boolean hasInternetConnection() {
1611        final ConnectivityManager cm =
1612                ContextCompat.getSystemService(this, ConnectivityManager.class);
1613        if (cm == null) {
1614            return true; // if internet connection can not be checked it is probably best to just
1615            // try
1616        }
1617        try {
1618            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
1619                final Network activeNetwork = cm.getActiveNetwork();
1620                final NetworkCapabilities capabilities =
1621                        activeNetwork == null ? null : cm.getNetworkCapabilities(activeNetwork);
1622                return capabilities != null
1623                        && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
1624            } else {
1625                final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
1626                return networkInfo != null
1627                        && (networkInfo.isConnected()
1628                                || networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET);
1629            }
1630        } catch (final RuntimeException e) {
1631            Log.d(Config.LOGTAG, "unable to check for internet connection", e);
1632            return true; // if internet connection can not be checked it is probably best to just
1633            // try
1634        }
1635    }
1636
1637    @SuppressLint("TrulyRandom")
1638    @Override
1639    public void onCreate() {
1640        com.cheogram.android.AndroidLoggingHandler.reset(new com.cheogram.android.AndroidLoggingHandler());
1641        java.util.logging.Logger.getLogger("").setLevel(java.util.logging.Level.FINEST);
1642        LibIdnXmppStringprep.setup();
1643        emojiSearch = new EmojiSearch(this);
1644        setTheme(R.style.Theme_Conversations3);
1645        ThemeHelper.applyCustomColors(this);
1646        if (Compatibility.runsTwentySix()) {
1647            mNotificationService.initializeChannels();
1648        }
1649        mChannelDiscoveryService.initializeMuclumbusService();
1650        mForceDuringOnCreate.set(Compatibility.runsAndTargetsTwentySix(this));
1651        toggleForegroundService();
1652        this.destroyed = false;
1653        OmemoSetting.load(this);
1654        try {
1655            Security.insertProviderAt(Conscrypt.newProvider(), 1);
1656        } catch (Throwable throwable) {
1657            Log.e(Config.LOGTAG, "unable to initialize security provider", throwable);
1658        }
1659        Resolver.init(this);
1660        updateMemorizingTrustManager();
1661        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
1662        final int cacheSize = maxMemory / 8;
1663        this.mDrawableCache = new LruCache<String, Drawable>(cacheSize) {
1664            @Override
1665            protected int sizeOf(final String key, final Drawable drawable) {
1666                if (drawable instanceof BitmapDrawable) {
1667                    Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
1668                    if (bitmap == null) return 1024;
1669
1670                    return bitmap.getByteCount() / 1024;
1671                } else if (drawable instanceof AvatarService.TextDrawable) {
1672                    return 50;
1673                } else {
1674                    return drawable.getIntrinsicWidth() * drawable.getIntrinsicHeight() * 40 / 1024;
1675                }
1676            }
1677        };
1678        if (mLastActivity == 0) {
1679            mLastActivity =
1680                    getPreferences().getLong(SETTING_LAST_ACTIVITY_TS, System.currentTimeMillis());
1681        }
1682
1683        Log.d(Config.LOGTAG, "initializing database...");
1684        this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
1685        Log.d(Config.LOGTAG, "restoring accounts...");
1686        this.accounts = databaseBackend.getAccounts();
1687        for (Account account : this.accounts) {
1688            final int color = getPreferences().getInt("account_color:" + account.getUuid(), 0);
1689            if (color != 0) account.setColor(color);
1690        }
1691        final SharedPreferences.Editor editor = getPreferences().edit();
1692        final boolean hasEnabledAccounts = hasEnabledAccounts();
1693        editor.putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
1694        editor.apply();
1695        toggleSetProfilePictureActivity(hasEnabledAccounts);
1696        reconfigurePushDistributor();
1697
1698        if (CallIntegration.hasSystemFeature(this)) {
1699            CallIntegrationConnectionService.togglePhoneAccountsAsync(this, this.accounts);
1700        }
1701
1702        restoreFromDatabase();
1703
1704        if (QuickConversationsService.isContactListIntegration(this)
1705                && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
1706                        == PackageManager.PERMISSION_GRANTED) {
1707            startContactObserver();
1708        }
1709        FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath);
1710        if (Compatibility.hasStoragePermission(this)) {
1711            Log.d(Config.LOGTAG, "starting file observer");
1712            FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::startWatching);
1713            FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
1714        }
1715        if (Config.supportOpenPgp()) {
1716            this.pgpServiceConnection =
1717                    new OpenPgpServiceConnection(
1718                            this,
1719                            "org.sufficientlysecure.keychain",
1720                            new OpenPgpServiceConnection.OnBound() {
1721                                @Override
1722                                public void onBound(final IOpenPgpService2 service) {
1723                                    for (Account account : accounts) {
1724                                        final PgpDecryptionService pgp =
1725                                                account.getPgpDecryptionService();
1726                                        if (pgp != null) {
1727                                            pgp.continueDecryption(true);
1728                                        }
1729                                    }
1730                                }
1731
1732                                @Override
1733                                public void onError(final Exception exception) {
1734                                    Log.e(
1735                                            Config.LOGTAG,
1736                                            "could not bind to OpenKeyChain",
1737                                            exception);
1738                                }
1739                            });
1740            this.pgpServiceConnection.bindToService();
1741        }
1742
1743        final PowerManager powerManager = getSystemService(PowerManager.class);
1744        if (powerManager != null) {
1745            this.wakeLock =
1746                    powerManager.newWakeLock(
1747                            PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service");
1748        }
1749
1750        toggleForegroundService();
1751        updateUnreadCountBadge();
1752        toggleScreenEventReceiver();
1753        final IntentFilter systemBroadcastFilter = new IntentFilter();
1754        scheduleNextIdlePing();
1755        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1756            systemBroadcastFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
1757        }
1758        systemBroadcastFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
1759        ContextCompat.registerReceiver(
1760                this,
1761                this.mInternalEventReceiver,
1762                systemBroadcastFilter,
1763                ContextCompat.RECEIVER_NOT_EXPORTED);
1764        final IntentFilter exportedBroadcastFilter = new IntentFilter();
1765        exportedBroadcastFilter.addAction(TorServiceUtils.ACTION_STATUS);
1766        ContextCompat.registerReceiver(
1767                this,
1768                this.mInternalRestrictedEventReceiver,
1769                exportedBroadcastFilter,
1770                ContextCompat.RECEIVER_EXPORTED);
1771        mForceDuringOnCreate.set(false);
1772        toggleForegroundService();
1773        rescanStickers();
1774        cleanupCache();
1775        internalPingExecutor.scheduleWithFixedDelay(
1776                this::manageAccountConnectionStatesInternal, 10, 10, TimeUnit.SECONDS);
1777        final SharedPreferences sharedPreferences =
1778                androidx.preference.PreferenceManager.getDefaultSharedPreferences(this);
1779        sharedPreferences.registerOnSharedPreferenceChangeListener(
1780                new SharedPreferences.OnSharedPreferenceChangeListener() {
1781                    @Override
1782                    public void onSharedPreferenceChanged(
1783                            SharedPreferences sharedPreferences, @Nullable String key) {
1784                        Log.d(Config.LOGTAG, "preference '" + key + "' has changed");
1785                        if (AppSettings.KEEP_FOREGROUND_SERVICE.equals(key)) {
1786                            toggleForegroundService();
1787                        }
1788                    }
1789                });
1790    }
1791
1792    private void checkForDeletedFiles() {
1793        if (destroyed) {
1794            Log.d(
1795                    Config.LOGTAG,
1796                    "Do not check for deleted files because service has been destroyed");
1797            return;
1798        }
1799        final long start = SystemClock.elapsedRealtime();
1800        final List<DatabaseBackend.FilePathInfo> relativeFilePaths =
1801                databaseBackend.getFilePathInfo();
1802        final List<DatabaseBackend.FilePathInfo> changed = new ArrayList<>();
1803        for (final DatabaseBackend.FilePathInfo filePath : relativeFilePaths) {
1804            if (destroyed) {
1805                Log.d(
1806                        Config.LOGTAG,
1807                        "Stop checking for deleted files because service has been destroyed");
1808                return;
1809            }
1810            final File file = fileBackend.getFileForPath(filePath.path);
1811            if (filePath.setDeleted(!file.exists())) {
1812                changed.add(filePath);
1813            }
1814        }
1815        final long duration = SystemClock.elapsedRealtime() - start;
1816        Log.d(
1817                Config.LOGTAG,
1818                "found "
1819                        + changed.size()
1820                        + " changed files on start up. total="
1821                        + relativeFilePaths.size()
1822                        + ". ("
1823                        + duration
1824                        + "ms)");
1825        if (changed.size() > 0) {
1826            databaseBackend.markFilesAsChanged(changed);
1827            markChangedFiles(changed);
1828        }
1829    }
1830
1831    public void startContactObserver() {
1832        getContentResolver()
1833                .registerContentObserver(
1834                        ContactsContract.Contacts.CONTENT_URI,
1835                        true,
1836                        new ContentObserver(null) {
1837                            @Override
1838                            public void onChange(boolean selfChange) {
1839                                super.onChange(selfChange);
1840                                if (restoredFromDatabaseLatch.getCount() == 0) {
1841                                    loadPhoneContacts();
1842                                }
1843                            }
1844                        });
1845    }
1846
1847    @Override
1848    public void onTrimMemory(int level) {
1849        super.onTrimMemory(level);
1850        if (level >= TRIM_MEMORY_COMPLETE) {
1851            Log.d(Config.LOGTAG, "clear cache due to low memory");
1852            getDrawableCache().evictAll();
1853        }
1854    }
1855
1856    @Override
1857    public void onDestroy() {
1858        try {
1859            unregisterReceiver(this.mInternalEventReceiver);
1860            unregisterReceiver(this.mInternalRestrictedEventReceiver);
1861            unregisterReceiver(this.mInternalScreenEventReceiver);
1862        } catch (final IllegalArgumentException e) {
1863            // ignored
1864        }
1865        destroyed = false;
1866        fileObserver.stopWatching();
1867        internalPingExecutor.shutdown();
1868        super.onDestroy();
1869    }
1870
1871    public void restartFileObserver() {
1872        Log.d(Config.LOGTAG, "restarting file observer");
1873        FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::restartWatching);
1874        FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
1875    }
1876
1877    public void toggleScreenEventReceiver() {
1878        if (awayWhenScreenLocked() && !manuallyChangePresence()) {
1879            final IntentFilter filter = new IntentFilter();
1880            filter.addAction(Intent.ACTION_SCREEN_ON);
1881            filter.addAction(Intent.ACTION_SCREEN_OFF);
1882            filter.addAction(Intent.ACTION_USER_PRESENT);
1883            registerReceiver(this.mInternalScreenEventReceiver, filter);
1884        } else {
1885            try {
1886                unregisterReceiver(this.mInternalScreenEventReceiver);
1887            } catch (IllegalArgumentException e) {
1888                // ignored
1889            }
1890        }
1891    }
1892
1893    public void toggleForegroundService() {
1894        toggleForegroundService(false, false);
1895    }
1896
1897    public void setOngoingCall(
1898            AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
1899        ongoingCall.set(new OngoingCall(id, media, reconnecting));
1900        toggleForegroundService(false, true);
1901    }
1902
1903    public void removeOngoingCall() {
1904        ongoingCall.set(null);
1905        toggleForegroundService(false, false);
1906    }
1907
1908    private void toggleForegroundService(boolean force, boolean needMic) {
1909        final boolean status;
1910        final OngoingCall ongoing = ongoingCall.get();
1911        final boolean ongoingVideoTranscoding = mOngoingVideoTranscoding.get();
1912        final int id;
1913        if (force
1914                || mForceDuringOnCreate.get()
1915                || ongoingVideoTranscoding
1916                || ongoing != null
1917                || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) {
1918            if (Compatibility.runsTwentySix()) {
1919                mNotificationService.initializeChannels();
1920            }
1921            final Notification notification;
1922            if (ongoing != null && !diallerIntegrationActive.get()) {
1923                notification = this.mNotificationService.getOngoingCallNotification(ongoing);
1924                id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
1925                startForegroundOrCatch(id, notification, true);
1926            } else if (ongoingVideoTranscoding) {
1927                notification = this.mNotificationService.getIndeterminateVideoTranscoding();
1928                id = NotificationService.ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID;
1929                startForegroundOrCatch(id, notification, false);
1930            } else {
1931                notification = this.mNotificationService.createForegroundNotification();
1932                id = NotificationService.FOREGROUND_NOTIFICATION_ID;
1933                startForegroundOrCatch(id, notification, needMic || ongoing != null || diallerIntegrationActive.get());
1934            }
1935            mNotificationService.notify(id, notification);
1936            status = true;
1937        } else {
1938            id = 0;
1939            stopForeground(true);
1940            status = false;
1941        }
1942
1943        for (final int toBeRemoved :
1944                Collections2.filter(
1945                        Arrays.asList(
1946                                NotificationService.FOREGROUND_NOTIFICATION_ID,
1947                                NotificationService.ONGOING_CALL_NOTIFICATION_ID,
1948                                NotificationService.ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID),
1949                        i -> i != id)) {
1950            mNotificationService.cancel(toBeRemoved);
1951        }
1952        Log.d(
1953                Config.LOGTAG,
1954                "ForegroundService: " + (status ? "on" : "off") + ", notification: " + id);
1955    }
1956
1957    private void startForegroundOrCatch(
1958            final int id, final Notification notification, final boolean requireMicrophone) {
1959        try {
1960            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
1961                final int foregroundServiceType;
1962                if (requireMicrophone
1963                        && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
1964                                == PackageManager.PERMISSION_GRANTED) {
1965                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
1966                    Log.d(Config.LOGTAG, "defaulting to microphone foreground service type");
1967                } else if (getSystemService(PowerManager.class)
1968                        .isIgnoringBatteryOptimizations(getPackageName())) {
1969                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED;
1970                } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
1971                        == PackageManager.PERMISSION_GRANTED) {
1972                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
1973                } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
1974                        == PackageManager.PERMISSION_GRANTED) {
1975                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
1976                } else {
1977                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
1978                    Log.w(Config.LOGTAG, "falling back to special use foreground service type");
1979                }
1980
1981                startForeground(id, notification, foregroundServiceType);
1982            } else {
1983                startForeground(id, notification);
1984            }
1985        } catch (final IllegalStateException | SecurityException e) {
1986            Log.e(Config.LOGTAG, "Could not start foreground service", e);
1987        }
1988    }
1989
1990    public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() {
1991        return !mOngoingVideoTranscoding.get()
1992                && ongoingCall.get() == null
1993                && Compatibility.keepForegroundService(this)
1994                && hasEnabledAccounts();
1995    }
1996
1997    @Override
1998    public void onTaskRemoved(final Intent rootIntent) {
1999        super.onTaskRemoved(rootIntent);
2000        if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts())
2001                || mOngoingVideoTranscoding.get()
2002                || ongoingCall.get() != null) {
2003            Log.d(Config.LOGTAG, "ignoring onTaskRemoved because foreground service is activated");
2004        } else {
2005            this.logoutAndSave(false);
2006        }
2007    }
2008
2009    private void logoutAndSave(boolean stop) {
2010        int activeAccounts = 0;
2011        for (final Account account : accounts) {
2012            if (account.isConnectionEnabled()) {
2013                databaseBackend.writeRoster(account.getRoster());
2014                activeAccounts++;
2015            }
2016            if (account.getXmppConnection() != null) {
2017                new Thread(() -> disconnect(account, false)).start();
2018            }
2019        }
2020        if (stop || activeAccounts == 0) {
2021            Log.d(Config.LOGTAG, "good bye");
2022            stopSelf();
2023        }
2024    }
2025
2026    private void schedulePostConnectivityChange() {
2027        final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
2028        if (alarmManager == null) {
2029            return;
2030        }
2031        final long triggerAtMillis =
2032                SystemClock.elapsedRealtime()
2033                        + (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL * 1000);
2034        final Intent intent = new Intent(this, SystemEventReceiver.class);
2035        intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE);
2036        try {
2037            final PendingIntent pendingIntent =
2038                    PendingIntent.getBroadcast(
2039                            this,
2040                            1,
2041                            intent,
2042                            s()
2043                                    ? PendingIntent.FLAG_IMMUTABLE
2044                                            | PendingIntent.FLAG_UPDATE_CURRENT
2045                                    : PendingIntent.FLAG_UPDATE_CURRENT);
2046            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
2047                alarmManager.setAndAllowWhileIdle(
2048                        AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
2049            } else {
2050                alarmManager.set(
2051                        AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
2052            }
2053        } catch (RuntimeException e) {
2054            Log.e(Config.LOGTAG, "unable to schedule alarm for post connectivity change", e);
2055        }
2056    }
2057
2058    public void scheduleWakeUpCall(final int seconds, final int requestCode) {
2059        scheduleWakeUpCall((seconds < 0 ? 1 : seconds + 1) * 1000L, requestCode);
2060    }
2061
2062    public void scheduleWakeUpCall(final long milliSeconds, final int requestCode) {
2063        final var timeToWake = SystemClock.elapsedRealtime() + milliSeconds;
2064        final var alarmManager = getSystemService(AlarmManager.class);
2065        final Intent intent = new Intent(this, SystemEventReceiver.class);
2066        intent.setAction(ACTION_PING);
2067        try {
2068            final PendingIntent pendingIntent =
2069                    PendingIntent.getBroadcast(
2070                            this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE);
2071            alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
2072        } catch (final RuntimeException e) {
2073            Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e);
2074        }
2075    }
2076
2077    private void scheduleNextIdlePing() {
2078        long timeUntilWake = Config.IDLE_PING_INTERVAL * 1000;
2079        final var now = System.currentTimeMillis();
2080        for (final var message : mScheduledMessages.values()) {
2081            if (message.getTimeSent() <= now) continue; // Just in case
2082            if (message.getTimeSent() - now < timeUntilWake) timeUntilWake = message.getTimeSent() - now;
2083        }
2084        final var timeToWake = SystemClock.elapsedRealtime() + timeUntilWake;
2085        final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
2086        if (alarmManager == null) {
2087            Log.d(Config.LOGTAG, "no alarm manager?");
2088            return;
2089        }
2090        final Intent intent = new Intent(this, SystemEventReceiver.class);
2091        intent.setAction(ACTION_IDLE_PING);
2092        try {
2093            final PendingIntent pendingIntent =
2094                    PendingIntent.getBroadcast(
2095                            this,
2096                            0,
2097                            intent,
2098                            s()
2099                                    ? PendingIntent.FLAG_IMMUTABLE
2100                                            | PendingIntent.FLAG_UPDATE_CURRENT
2101                                    : PendingIntent.FLAG_UPDATE_CURRENT);
2102            alarmManager.setAndAllowWhileIdle(
2103                    AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
2104        } catch (RuntimeException e) {
2105            Log.d(Config.LOGTAG, "unable to schedule alarm for idle ping", e);
2106        }
2107    }
2108
2109    public XmppConnection createConnection(final Account account) {
2110        final XmppConnection connection = new XmppConnection(account, this);
2111        connection.setOnStatusChangedListener(this.statusListener);
2112        connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket));
2113        connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
2114        connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
2115        connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService);
2116        AxolotlService axolotlService = account.getAxolotlService();
2117        if (axolotlService != null) {
2118            connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
2119        }
2120        return connection;
2121    }
2122
2123    public void sendChatState(Conversation conversation) {
2124        if (sendChatStates()) {
2125            final var packet = mMessageGenerator.generateChatState(conversation);
2126            sendMessagePacket(conversation.getAccount(), packet);
2127        }
2128    }
2129
2130    private void sendFileMessage(
2131            final Message message, final boolean delay, final boolean forceP2P, final Runnable cb) {
2132        final var account = message.getConversation().getAccount();
2133        Log.d(
2134                Config.LOGTAG,
2135                account.getJid().asBareJid() + ": send file message. forceP2P=" + forceP2P);
2136        if ((account.httpUploadAvailable(fileBackend.getFile(message, false).getSize())
2137                        || message.getConversation().getMode() == Conversation.MODE_MULTI)
2138                && !forceP2P) {
2139            mHttpConnectionManager.createNewUploadConnection(message, delay, cb);
2140        } else {
2141            mJingleConnectionManager.startJingleFileTransfer(message);
2142            if (cb != null) cb.run();
2143        }
2144    }
2145
2146    public void sendMessage(final Message message) {
2147        sendMessage(message, false, false, false, false, null);
2148    }
2149
2150    public void sendMessage(final Message message, final Runnable cb) {
2151        sendMessage(message, false, false, false, false, cb);
2152    }
2153
2154    private void sendMessage(final Message message, final boolean resend, final boolean previewedLinks, final boolean delay, final Runnable cb) {
2155        sendMessage(message, resend, previewedLinks, delay, false, cb);
2156    }
2157
2158    private void sendMessage(
2159            final Message message,
2160            final boolean resend,
2161            final boolean previewedLinks,
2162            final boolean delay,
2163            final boolean forceP2P,
2164            final Runnable cb) {
2165        final Account account = message.getConversation().getAccount();
2166        if (account.setShowErrorNotification(true)) {
2167            databaseBackend.updateAccount(account);
2168            mNotificationService.updateErrorNotification();
2169        }
2170        final Conversation conversation = (Conversation) message.getConversation();
2171        account.deactivateGracePeriod();
2172
2173        if (QuickConversationsService.isQuicksy()
2174                && conversation.getMode() == Conversation.MODE_SINGLE) {
2175            final Contact contact = conversation.getContact();
2176            if (!contact.showInRoster() && contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) {
2177                Log.d(
2178                        Config.LOGTAG,
2179                        account.getJid().asBareJid()
2180                                + ": adding "
2181                                + contact.getJid()
2182                                + " on sending message");
2183                createContact(contact, true);
2184            }
2185        }
2186
2187        im.conversations.android.xmpp.model.stanza.Message packet = null;
2188        final boolean addToConversation = !message.edited() && message.getRawBody() != null;
2189        boolean saveInDb = addToConversation;
2190        message.setStatus(Message.STATUS_WAITING);
2191
2192        if (message.getEncryption() != Message.ENCRYPTION_NONE
2193                && conversation.getMode() == Conversation.MODE_MULTI
2194                && conversation.isPrivateAndNonAnonymous()) {
2195            if (conversation.setAttribute(
2196                    Conversation.ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, true)) {
2197                databaseBackend.updateConversation(conversation);
2198            }
2199        }
2200
2201        final boolean inProgressJoin = isJoinInProgress(conversation);
2202
2203        if (message.getCounterpart() == null && !message.isPrivateMessage()) {
2204            message.setCounterpart(message.getConversation().getJid().asBareJid());
2205        }
2206
2207        boolean waitForPreview = false;
2208        if (getPreferences().getBoolean("send_link_previews", true) && !previewedLinks && !message.needsUploading() && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
2209            message.clearLinkDescriptions();
2210            final List<URI> links = message.getLinks();
2211            if (!links.isEmpty()) {
2212                waitForPreview = true;
2213                if (account.isOnlineAndConnected()) {
2214                    FILE_ATTACHMENT_EXECUTOR.execute(() -> {
2215                        for (URI link : links) {
2216                            if ("https".equals(link.getScheme())) {
2217                                try {
2218                                    HttpUrl url = HttpUrl.parse(link.toString());
2219                                    OkHttpClient http = getHttpConnectionManager().buildHttpClient(url, account, 5, false);
2220                                    final var request = new okhttp3.Request.Builder().url(url).head().build();
2221                                    okhttp3.Response response = null;
2222                                    if ("www.amazon.com".equals(link.getHost()) || "www.amazon.ca".equals(link.getHost())) {
2223                                        // Amazon blocks HEAD
2224                                        response = new okhttp3.Response.Builder().request(request).protocol(okhttp3.Protocol.HTTP_1_1).code(200).message("OK").addHeader("Content-Type", "text/html").build();
2225                                    } else {
2226                                        response = http.newCall(request).execute();
2227                                    }
2228                                    final String mimeType = response.header("Content-Type") == null ? "" : response.header("Content-Type");
2229                                    final boolean image = mimeType.startsWith("image/");
2230                                    final boolean audio = mimeType.startsWith("audio/");
2231                                    final boolean video = mimeType.startsWith("video/");
2232                                    final boolean pdf = mimeType.equals("application/pdf");
2233                                    final boolean html = mimeType.startsWith("text/html") || mimeType.startsWith("application/xhtml+xml");
2234                                    if (response.isSuccessful() && (image || audio || video || pdf)) {
2235                                        Message.FileParams params = message.getFileParams();
2236                                        params.url = url.toString();
2237                                        if (response.header("Content-Length") != null) params.size = Long.parseLong(response.header("Content-Length"), 10);
2238                                        if (!Message.configurePrivateFileMessage(message)) {
2239                                            message.setType(image ? Message.TYPE_IMAGE : Message.TYPE_FILE);
2240                                        }
2241                                        params.setName(HttpConnectionManager.extractFilenameFromResponse(response));
2242
2243                                        if (link.toString().equals(message.getRawBody())) {
2244                                            Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
2245                                            fallback.addChild("body", "urn:xmpp:fallback:0");
2246                                            message.addPayload(fallback);
2247                                        } else if (message.getRawBody().indexOf(link.toString()) >= 0) {
2248                                            // Part of the real body, not just a fallback
2249                                            Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
2250                                            fallback.addChild("body", "urn:xmpp:fallback:0")
2251                                                .setAttribute("start", "0")
2252                                                .setAttribute("end", "0");
2253                                            message.addPayload(fallback);
2254                                        }
2255
2256                                        final int encryption = message.getEncryption();
2257                                        getHttpConnectionManager().createNewDownloadConnection(message, false, (file) -> {
2258                                            message.setEncryption(encryption);
2259                                            synchronized (message.getConversation()) {
2260                                                if (message.getStatus() == Message.STATUS_WAITING) sendMessage(message, true, true, false, cb);
2261                                            }
2262                                        });
2263                                        return;
2264                                    } else if (response.isSuccessful() && html) {
2265                                        Semaphore waiter = new Semaphore(0);
2266                                        OpenGraphParser.Builder openGraphBuilder = new OpenGraphParser.Builder(new OpenGraphCallback() {
2267                                            @Override
2268                                            public void onPostResponse(OpenGraphResult result) {
2269                                                Element rdf = new Element("Description", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
2270                                                rdf.setAttribute("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
2271                                                rdf.setAttribute("rdf:about", link.toString());
2272                                                if (result.getTitle() != null && !"".equals(result.getTitle())) {
2273                                                    rdf.addChild("title", "https://ogp.me/ns#").setContent(result.getTitle());
2274                                                }
2275                                                if (result.getDescription() != null && !"".equals(result.getDescription())) {
2276                                                    rdf.addChild("description", "https://ogp.me/ns#").setContent(result.getDescription());
2277                                                }
2278                                                if (result.getUrl() != null) {
2279                                                    rdf.addChild("url", "https://ogp.me/ns#").setContent(result.getUrl());
2280                                                }
2281                                                if (result.getImage() != null) {
2282                                                    rdf.addChild("image", "https://ogp.me/ns#").setContent(result.getImage());
2283                                                }
2284                                                if (result.getType() != null) {
2285                                                    rdf.addChild("type", "https://ogp.me/ns#").setContent(result.getType());
2286                                                }
2287                                                if (result.getSiteName() != null) {
2288                                                    rdf.addChild("site_name", "https://ogp.me/ns#").setContent(result.getSiteName());
2289                                                }
2290                                                if (result.getVideo() != null) {
2291                                                    rdf.addChild("video", "https://ogp.me/ns#").setContent(result.getVideo());
2292                                                }
2293                                                message.addPayload(rdf);
2294                                                waiter.release();
2295                                            }
2296
2297                                            public void onError(String error) {
2298                                                waiter.release();
2299                                            }
2300                                        })
2301                                            .showNullOnEmpty(true)
2302                                            .maxBodySize(90000)
2303                                            .timeout(5000);
2304                                        if (useTorToConnect()) {
2305                                            openGraphBuilder = openGraphBuilder.jsoupProxy(new JsoupProxy("127.0.0.1", 8118));
2306                                        }
2307                                        openGraphBuilder.build().parse(link.toString());
2308                                        waiter.tryAcquire(10L, TimeUnit.SECONDS);
2309                                    }
2310                                } catch (final IOException | InterruptedException e) {  }
2311                            }
2312                        }
2313                        synchronized (message.getConversation()) {
2314                            if (message.getStatus() == Message.STATUS_WAITING) sendMessage(message, true, true, false, cb);
2315                        }
2316                    });
2317                }
2318            }
2319        }
2320
2321        boolean passedCbOn = false;
2322        if (account.isOnlineAndConnected() && !inProgressJoin && !waitForPreview && message.getTimeSent() <= System.currentTimeMillis()) {
2323            switch (message.getEncryption()) {
2324                case Message.ENCRYPTION_NONE:
2325                    if (message.needsUploading()) {
2326                        if (account.httpUploadAvailable(
2327                                        fileBackend.getFile(message, false).getSize())
2328                                || conversation.getMode() == Conversation.MODE_MULTI
2329                                || message.fixCounterpart()) {
2330                            this.sendFileMessage(message, delay, forceP2P, cb);
2331                            passedCbOn = true;
2332                        } else {
2333                            break;
2334                        }
2335                    } else {
2336                        packet = mMessageGenerator.generateChat(message);
2337                    }
2338                    break;
2339                case Message.ENCRYPTION_PGP:
2340                case Message.ENCRYPTION_DECRYPTED:
2341                    if (message.needsUploading()) {
2342                        if (account.httpUploadAvailable(
2343                                        fileBackend.getFile(message, false).getSize())
2344                                || conversation.getMode() == Conversation.MODE_MULTI
2345                                || message.fixCounterpart()) {
2346                            this.sendFileMessage(message, delay, forceP2P, cb);
2347                            passedCbOn = true;
2348                        } else {
2349                            break;
2350                        }
2351                    } else {
2352                        packet = mMessageGenerator.generatePgpChat(message);
2353                    }
2354                    break;
2355                case Message.ENCRYPTION_AXOLOTL:
2356                    message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
2357                    if (message.needsUploading()) {
2358                        if (account.httpUploadAvailable(
2359                                        fileBackend.getFile(message, false).getSize())
2360                                || conversation.getMode() == Conversation.MODE_MULTI
2361                                || message.fixCounterpart()) {
2362                            this.sendFileMessage(message, delay, forceP2P, cb);
2363                            passedCbOn = true;
2364                        } else {
2365                            break;
2366                        }
2367                    } else {
2368                        XmppAxolotlMessage axolotlMessage =
2369                                account.getAxolotlService().fetchAxolotlMessageFromCache(message);
2370                        if (axolotlMessage == null) {
2371                            account.getAxolotlService().preparePayloadMessage(message, delay);
2372                        } else {
2373                            packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
2374                        }
2375                    }
2376                    break;
2377            }
2378            if (packet != null) {
2379                if (account.getXmppConnection().getFeatures().sm()
2380                        || (conversation.getMode() == Conversation.MODE_MULTI
2381                                && message.getCounterpart().isBareJid())) {
2382                    message.setStatus(Message.STATUS_UNSEND);
2383                } else {
2384                    message.setStatus(Message.STATUS_SEND);
2385                }
2386            }
2387        } else {
2388            switch (message.getEncryption()) {
2389                case Message.ENCRYPTION_DECRYPTED:
2390                    if (!message.needsUploading()) {
2391                        String pgpBody = message.getEncryptedBody();
2392                        String decryptedBody = message.getBody();
2393                        message.setBody(pgpBody); // TODO might throw NPE
2394                        message.setEncryption(Message.ENCRYPTION_PGP);
2395                        if (message.edited()) {
2396                            message.setBody(decryptedBody);
2397                            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
2398                            if (!databaseBackend.updateMessage(message, message.getEditedId())) {
2399                                Log.e(Config.LOGTAG, "error updated message in DB after edit");
2400                            }
2401                            updateConversationUi();
2402                            if (!waitForPreview && cb != null) cb.run();
2403                            return;
2404                        } else {
2405                            databaseBackend.createMessage(message);
2406                            saveInDb = false;
2407                            message.setBody(decryptedBody);
2408                            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
2409                        }
2410                    }
2411                    break;
2412                case Message.ENCRYPTION_AXOLOTL:
2413                    message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
2414                    break;
2415            }
2416        }
2417
2418        synchronized (mScheduledMessages) {
2419            if (message.getTimeSent() > System.currentTimeMillis()) {
2420                mScheduledMessages.put(message.getUuid(), message);
2421                scheduleNextIdlePing();
2422            } else {
2423                mScheduledMessages.remove(message.getUuid());
2424            }
2425        }
2426
2427        boolean mucMessage =
2428                conversation.getMode() == Conversation.MODE_MULTI && !message.isPrivateMessage();
2429        if (mucMessage) {
2430            message.setCounterpart(conversation.getMucOptions().getSelf().getFullJid());
2431        }
2432
2433        if (resend) {
2434            if (packet != null && addToConversation) {
2435                if (account.getXmppConnection().getFeatures().sm() || mucMessage) {
2436                    markMessage(message, Message.STATUS_UNSEND);
2437                } else {
2438                    markMessage(message, Message.STATUS_SEND);
2439                }
2440            }
2441        } else {
2442            if (addToConversation) {
2443                conversation.add(message);
2444            }
2445            if (saveInDb) {
2446                databaseBackend.createMessage(message);
2447            } else if (message.edited()) {
2448                if (!databaseBackend.updateMessage(message, message.getEditedId())) {
2449                    Log.e(Config.LOGTAG, "error updated message in DB after edit");
2450                }
2451            }
2452            updateConversationUi();
2453        }
2454        if (packet != null) {
2455            if (delay) {
2456                mMessageGenerator.addDelay(packet, message.getTimeSent());
2457            }
2458            if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
2459                if (this.sendChatStates()) {
2460                    packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
2461                }
2462            }
2463            sendMessagePacket(account, packet);
2464            if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.hasCustomEmoji()) {
2465                if (message.getConversation() instanceof Conversation) presenceToMuc((Conversation) message.getConversation());
2466            }
2467        }
2468        if (!waitForPreview && !passedCbOn && cb != null) cb.run();
2469    }
2470
2471    private boolean isJoinInProgress(final Conversation conversation) {
2472        final Account account = conversation.getAccount();
2473        synchronized (account.inProgressConferenceJoins) {
2474            if (conversation.getMode() == Conversational.MODE_MULTI) {
2475                final boolean inProgress = account.inProgressConferenceJoins.contains(conversation);
2476                final boolean pending = account.pendingConferenceJoins.contains(conversation);
2477                final boolean inProgressJoin = inProgress || pending;
2478                if (inProgressJoin) {
2479                    Log.d(
2480                            Config.LOGTAG,
2481                            account.getJid().asBareJid()
2482                                    + ": holding back message to group. inProgress="
2483                                    + inProgress
2484                                    + ", pending="
2485                                    + pending);
2486                }
2487                return inProgressJoin;
2488            } else {
2489                return false;
2490            }
2491        }
2492    }
2493
2494    private void sendUnsentMessages(final Conversation conversation) {
2495        synchronized (conversation) {
2496            conversation.findWaitingMessages(message -> resendMessage(message, true));
2497        }
2498    }
2499
2500    public void resendMessage(final Message message, final boolean delay) {
2501        sendMessage(message, true, false, delay, false, null);
2502    }
2503
2504    public void resendMessage(final Message message, final boolean delay, final Runnable cb) {
2505        sendMessage(message, true, false, delay, false, cb);
2506    }
2507
2508    public void resendMessage(final Message message, final boolean delay, final boolean previewedLinks) {
2509        sendMessage(message, true, previewedLinks, delay, false, null);
2510    }
2511
2512    public Pair<Account,Account> onboardingIncomplete() {
2513        if (getAccounts().size() != 2) return null;
2514        Account onboarding = null;
2515        Account newAccount = null;
2516        for (final Account account : getAccounts()) {
2517            if (account.getJid().getDomain().equals(Config.ONBOARDING_DOMAIN)) {
2518                onboarding = account;
2519            } else {
2520                newAccount = account;
2521            }
2522        }
2523
2524        if (onboarding != null && newAccount != null) {
2525            return new Pair<>(onboarding, newAccount);
2526        }
2527
2528        return null;
2529    }
2530
2531    public boolean isOnboarding() {
2532        return getAccounts().size() == 1 && getAccounts().get(0).getJid().getDomain().equals(Config.ONBOARDING_DOMAIN);
2533    }
2534
2535    public void requestEasyOnboardingInvite(
2536            final Account account, final EasyOnboardingInvite.OnInviteRequested callback) {
2537        final XmppConnection connection = account.getXmppConnection();
2538        final Jid jid =
2539                connection == null
2540                        ? null
2541                        : connection.getJidForCommand(Namespace.EASY_ONBOARDING_INVITE);
2542        if (jid == null) {
2543            callback.inviteRequestFailed(
2544                    getString(R.string.server_does_not_support_easy_onboarding_invites));
2545            return;
2546        }
2547        final Iq request = new Iq(Iq.Type.SET);
2548        request.setTo(jid);
2549        final Element command = request.addChild("command", Namespace.COMMANDS);
2550        command.setAttribute("node", Namespace.EASY_ONBOARDING_INVITE);
2551        command.setAttribute("action", "execute");
2552        sendIqPacket(
2553                account,
2554                request,
2555                (response) -> {
2556                    if (response.getType() == Iq.Type.RESULT) {
2557                        final Element resultCommand =
2558                                response.findChild("command", Namespace.COMMANDS);
2559                        final Element x =
2560                                resultCommand == null
2561                                        ? null
2562                                        : resultCommand.findChild("x", Namespace.DATA);
2563                        if (x != null) {
2564                            final Data data = Data.parse(x);
2565                            final String uri = data.getValue("uri");
2566                            final String landingUrl = data.getValue("landing-url");
2567                            if (uri != null) {
2568                                final EasyOnboardingInvite invite =
2569                                        new EasyOnboardingInvite(
2570                                                jid.getDomain().toString(), uri, landingUrl);
2571                                callback.inviteRequested(invite);
2572                                return;
2573                            }
2574                        }
2575                        callback.inviteRequestFailed(getString(R.string.unable_to_parse_invite));
2576                        Log.d(Config.LOGTAG, response.toString());
2577                    } else if (response.getType() == Iq.Type.ERROR) {
2578                        callback.inviteRequestFailed(IqParser.errorMessage(response));
2579                    } else {
2580                        callback.inviteRequestFailed(getString(R.string.remote_server_timeout));
2581                    }
2582                });
2583    }
2584
2585    public void fetchBookmarks(final Account account) {
2586        final Iq iqPacket = new Iq(Iq.Type.GET);
2587        final Element query = iqPacket.query("jabber:iq:private");
2588        query.addChild("storage", Namespace.BOOKMARKS);
2589        final Consumer<Iq> callback =
2590                (response) -> {
2591                    if (response.getType() == Iq.Type.RESULT) {
2592                        final Element query1 = response.query();
2593                        final Element storage = query1.findChild("storage", "storage:bookmarks");
2594                        Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
2595                        processBookmarksInitial(account, bookmarks, false);
2596                    } else {
2597                        Log.d(
2598                                Config.LOGTAG,
2599                                account.getJid().asBareJid() + ": could not fetch bookmarks");
2600                    }
2601                };
2602        sendIqPacket(account, iqPacket, callback);
2603    }
2604
2605    public void fetchBookmarks2(final Account account) {
2606        final Iq retrieve = mIqGenerator.retrieveBookmarks();
2607        sendIqPacket(
2608                account,
2609                retrieve,
2610                (response) -> {
2611                    if (response.getType() == Iq.Type.RESULT) {
2612                        final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB);
2613                        final Map<Jid, Bookmark> bookmarks =
2614                                Bookmark.parseFromPubSub(pubsub, account);
2615                        processBookmarksInitial(account, bookmarks, true);
2616                    }
2617                });
2618    }
2619
2620    public void fetchMessageDisplayedSynchronization(final Account account) {
2621        Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds");
2622        final var retrieve = mIqGenerator.retrieveMds();
2623        sendIqPacket(
2624                account,
2625                retrieve,
2626                (response) -> {
2627                    if (response.getType() != Iq.Type.RESULT) {
2628                        return;
2629                    }
2630                    final var pubSub = response.findChild("pubsub", Namespace.PUBSUB);
2631                    final Element items = pubSub == null ? null : pubSub.findChild("items");
2632                    if (items == null
2633                            || !Namespace.MDS_DISPLAYED.equals(items.getAttribute("node"))) {
2634                        return;
2635                    }
2636                    for (final Element child : items.getChildren()) {
2637                        if ("item".equals(child.getName())) {
2638                            processMdsItem(account, child);
2639                        }
2640                    }
2641                });
2642    }
2643
2644    public void processMdsItem(final Account account, final Element item) {
2645        final Jid jid =
2646                item == null ? null : Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("id"));
2647        if (jid == null) {
2648            return;
2649        }
2650        final Element displayed = item.findChild("displayed", Namespace.MDS_DISPLAYED);
2651        final Element stanzaId =
2652                displayed == null ? null : displayed.findChild("stanza-id", Namespace.STANZA_IDS);
2653        final String id = stanzaId == null ? null : stanzaId.getAttribute("id");
2654        final Conversation conversation = find(account, jid);
2655        if (id != null && conversation != null) {
2656            conversation.setDisplayState(id);
2657            markReadUpToStanzaId(conversation, id);
2658        }
2659    }
2660
2661    public void markReadUpToStanzaId(final Conversation conversation, final String stanzaId) {
2662        final Message message = conversation.findMessageWithServerMsgId(stanzaId);
2663        if (message == null) { // do we want to check if isRead?
2664            return;
2665        }
2666        markReadUpTo(conversation, message);
2667    }
2668
2669    public void markReadUpTo(final Conversation conversation, final Message message) {
2670        final boolean isDismissNotification = isDismissNotification(message);
2671        final var uuid = message.getUuid();
2672        Log.d(
2673                Config.LOGTAG,
2674                conversation.getAccount().getJid().asBareJid()
2675                        + ": mark "
2676                        + conversation.getJid().asBareJid()
2677                        + " as read up to "
2678                        + uuid);
2679        markRead(conversation, uuid, isDismissNotification);
2680    }
2681
2682    private static boolean isDismissNotification(final Message message) {
2683        Message next = message.next();
2684        while (next != null) {
2685            if (message.getStatus() == Message.STATUS_RECEIVED) {
2686                return false;
2687            }
2688            next = next.next();
2689        }
2690        return true;
2691    }
2692
2693    public void processBookmarksInitial(
2694            final Account account, final Map<Jid, Bookmark> bookmarks, final boolean pep) {
2695        final Set<Jid> previousBookmarks = account.getBookmarkedJids();
2696        for (final Bookmark bookmark : bookmarks.values()) {
2697            previousBookmarks.remove(bookmark.getJid().asBareJid());
2698            processModifiedBookmark(bookmark, pep);
2699        }
2700        if (pep) {
2701            processDeletedBookmarks(account, previousBookmarks);
2702        }
2703        account.setBookmarks(bookmarks);
2704    }
2705
2706    public void processDeletedBookmarks(final Account account, final Collection<Jid> bookmarks) {
2707        Log.d(
2708                Config.LOGTAG,
2709                account.getJid().asBareJid()
2710                        + ": "
2711                        + bookmarks.size()
2712                        + " bookmarks have been removed");
2713        for (final Jid bookmark : bookmarks) {
2714            processDeletedBookmark(account, bookmark);
2715        }
2716    }
2717
2718    public void processDeletedBookmark(final Account account, final Jid jid) {
2719        final Conversation conversation = find(account, jid);
2720        if (conversation == null) {
2721            return;
2722        }
2723        Log.d(
2724                Config.LOGTAG,
2725                account.getJid().asBareJid() + ": archiving MUC " + jid + " after PEP update");
2726        archiveConversation(conversation, false);
2727    }
2728
2729    private void processModifiedBookmark(final Bookmark bookmark, final boolean pep) {
2730        final Account account = bookmark.getAccount();
2731        Conversation conversation = find(bookmark);
2732        if (conversation != null) {
2733            if (conversation.getMode() != Conversation.MODE_MULTI) {
2734                return;
2735            }
2736            bookmark.setConversation(conversation);
2737            if (pep && !bookmark.autojoin()) {
2738                Log.d(
2739                        Config.LOGTAG,
2740                        account.getJid().asBareJid()
2741                                + ": archiving conference ("
2742                                + conversation.getJid()
2743                                + ") after receiving pep");
2744                archiveConversation(conversation, false);
2745            } else {
2746                final MucOptions mucOptions = conversation.getMucOptions();
2747                if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) {
2748                    final String current = mucOptions.getActualNick();
2749                    final String proposed = mucOptions.getProposedNickPure();
2750                    if (current != null && !current.equals(proposed)) {
2751                        Log.d(
2752                                Config.LOGTAG,
2753                                account.getJid().asBareJid()
2754                                        + ": proposed nick changed after bookmark push "
2755                                        + current
2756                                        + "->"
2757                                        + proposed);
2758                        joinMuc(conversation);
2759                    }
2760                } else {
2761                    checkMucRequiresRename(conversation);
2762                }
2763            }
2764        } else if (bookmark.autojoin()) {
2765            conversation =
2766                    findOrCreateConversation(account, bookmark.getFullJid(), true, true, false);
2767            bookmark.setConversation(conversation);
2768        }
2769    }
2770
2771    public void processModifiedBookmark(final Bookmark bookmark) {
2772        processModifiedBookmark(bookmark, true);
2773    }
2774
2775    public void ensureBookmarkIsAutoJoin(final Conversation conversation) {
2776        final var account = conversation.getAccount();
2777        final var existingBookmark = conversation.getBookmark();
2778        if (existingBookmark == null) {
2779            final var bookmark = new Bookmark(account, conversation.getJid().asBareJid());
2780            bookmark.setAutojoin(true);
2781            createBookmark(account, bookmark);
2782        } else {
2783            if (existingBookmark.autojoin()) {
2784                return;
2785            }
2786            existingBookmark.setAutojoin(true);
2787            createBookmark(account, existingBookmark);
2788        }
2789    }
2790
2791    public void createBookmark(final Account account, final Bookmark bookmark) {
2792        account.putBookmark(bookmark);
2793        final XmppConnection connection = account.getXmppConnection();
2794        if (connection == null) {
2795            Log.d(
2796                    Config.LOGTAG,
2797                    account.getJid().asBareJid() + ": no connection. ignoring bookmark creation");
2798        } else if (connection.getFeatures().bookmarks2()) {
2799            Log.d(
2800                    Config.LOGTAG,
2801                    account.getJid().asBareJid() + ": pushing bookmark via Bookmarks 2");
2802            final Element item = mIqGenerator.publishBookmarkItem(bookmark);
2803            pushNodeAndEnforcePublishOptions(
2804                    account,
2805                    Namespace.BOOKMARKS2,
2806                    item,
2807                    bookmark.getJid().asBareJid().toString(),
2808                    PublishOptions.persistentWhitelistAccessMaxItems());
2809        } else if (connection.getFeatures().bookmarksConversion()) {
2810            pushBookmarksPep(account);
2811        } else {
2812            pushBookmarksPrivateXml(account);
2813        }
2814    }
2815
2816    public void deleteBookmark(final Account account, final Bookmark bookmark) {
2817        if (bookmark.getJid().toString().equals("discuss@conference.soprani.ca")) {
2818            getPreferences().edit().putBoolean("cheogram_sopranica_bookmark_deleted", true).apply();
2819        }
2820        account.removeBookmark(bookmark);
2821        final XmppConnection connection = account.getXmppConnection();
2822        if (connection == null) return;
2823
2824        if (connection.getFeatures().bookmarks2()) {
2825            final Iq request =
2826                    mIqGenerator.deleteItem(
2827                            Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toString());
2828            Log.d(
2829                    Config.LOGTAG,
2830                    account.getJid().asBareJid() + ": removing bookmark via Bookmarks 2");
2831            sendIqPacket(
2832                    account,
2833                    request,
2834                    (response) -> {
2835                        if (response.getType() == Iq.Type.ERROR) {
2836                            Log.d(
2837                                    Config.LOGTAG,
2838                                    account.getJid().asBareJid()
2839                                            + ": unable to delete bookmark "
2840                                            + response.getErrorCondition());
2841                        }
2842                    });
2843        } else if (connection.getFeatures().bookmarksConversion()) {
2844            pushBookmarksPep(account);
2845        } else {
2846            pushBookmarksPrivateXml(account);
2847        }
2848    }
2849
2850    private void pushBookmarksPrivateXml(Account account) {
2851        if (!account.areBookmarksLoaded()) return;
2852
2853        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml");
2854        final Iq iqPacket = new Iq(Iq.Type.SET);
2855        Element query = iqPacket.query("jabber:iq:private");
2856        Element storage = query.addChild("storage", "storage:bookmarks");
2857        for (final Bookmark bookmark : account.getBookmarks()) {
2858            storage.addChild(bookmark);
2859        }
2860        sendIqPacket(account, iqPacket, mDefaultIqHandler);
2861    }
2862
2863    private void pushBookmarksPep(Account account) {
2864        if (!account.areBookmarksLoaded()) return;
2865
2866        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via pep");
2867        final Element storage = new Element("storage", "storage:bookmarks");
2868        for (final Bookmark bookmark : account.getBookmarks()) {
2869            storage.addChild(bookmark);
2870        }
2871        pushNodeAndEnforcePublishOptions(
2872                account,
2873                Namespace.BOOKMARKS,
2874                storage,
2875                "current",
2876                PublishOptions.persistentWhitelistAccess());
2877    }
2878
2879    private void pushNodeAndEnforcePublishOptions(
2880            final Account account,
2881            final String node,
2882            final Element element,
2883            final String id,
2884            final Bundle options) {
2885        pushNodeAndEnforcePublishOptions(account, node, element, id, options, true);
2886    }
2887
2888    private void pushNodeAndEnforcePublishOptions(
2889            final Account account,
2890            final String node,
2891            final Element element,
2892            final String id,
2893            final Bundle options,
2894            final boolean retry) {
2895        final Iq packet = mIqGenerator.publishElement(node, element, id, options);
2896        sendIqPacket(
2897                account,
2898                packet,
2899                (response) -> {
2900                    if (response.getType() == Iq.Type.RESULT) {
2901                        return;
2902                    }
2903                    if (retry && PublishOptions.preconditionNotMet(response)) {
2904                        pushNodeConfiguration(
2905                                account,
2906                                node,
2907                                options,
2908                                new OnConfigurationPushed() {
2909                                    @Override
2910                                    public void onPushSucceeded() {
2911                                        pushNodeAndEnforcePublishOptions(
2912                                                account, node, element, id, options, false);
2913                                    }
2914
2915                                    @Override
2916                                    public void onPushFailed() {
2917                                        Log.d(
2918                                                Config.LOGTAG,
2919                                                account.getJid().asBareJid()
2920                                                        + ": unable to push node configuration ("
2921                                                        + node
2922                                                        + ")");
2923                                    }
2924                                });
2925                    } else {
2926                        Log.d(
2927                                Config.LOGTAG,
2928                                account.getJid().asBareJid()
2929                                        + ": error publishing "
2930                                        + node
2931                                        + " (retry="
2932                                        + retry
2933                                        + ") "
2934                                        + response);
2935                    }
2936                });
2937    }
2938
2939    private void restoreFromDatabase() {
2940        synchronized (this.conversations) {
2941            final Map<String, Account> accountLookupTable =
2942                    ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
2943            Log.d(Config.LOGTAG, "restoring conversations...");
2944            final long startTimeConversationsRestore = SystemClock.elapsedRealtime();
2945            this.conversations.addAll(
2946                    databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
2947            for (Iterator<Conversation> iterator = conversations.listIterator();
2948                    iterator.hasNext(); ) {
2949                Conversation conversation = iterator.next();
2950                Account account = accountLookupTable.get(conversation.getAccountUuid());
2951                if (account != null) {
2952                    conversation.setAccount(account);
2953                } else {
2954                    Log.e(Config.LOGTAG, "unable to restore Conversations with " + conversation.getJid());
2955                    conversations.remove(conversation);
2956                }
2957            }
2958            long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore;
2959            Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms");
2960            Runnable runnable = () -> {
2961                if (DatabaseBackend.requiresMessageIndexRebuild()) {
2962                    DatabaseBackend.getInstance(this).rebuildMessagesIndex();
2963                }
2964                mutedMucUsers = databaseBackend.loadMutedMucUsers();
2965                final long deletionDate = getAutomaticMessageDeletionDate();
2966                mLastExpiryRun.set(SystemClock.elapsedRealtime());
2967                if (deletionDate > 0) {
2968                    Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate));
2969                    databaseBackend.expireOldMessages(deletionDate);
2970                }
2971                Log.d(Config.LOGTAG, "restoring roster...");
2972                for (final Account account : accounts) {
2973                    databaseBackend.readRoster(account.getRoster());
2974                    account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage
2975                }
2976                getDrawableCache().evictAll();
2977                loadPhoneContacts();
2978                Log.d(Config.LOGTAG, "restoring messages...");
2979                final long startMessageRestore = SystemClock.elapsedRealtime();
2980                final Conversation quickLoad = QuickLoader.get(this.conversations);
2981                if (quickLoad != null) {
2982                    restoreMessages(quickLoad);
2983                    updateConversationUi();
2984                    final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
2985                    Log.d(Config.LOGTAG, "quickly restored " + quickLoad.getName() + " after " + diffMessageRestore + "ms");
2986                }
2987                for (Conversation conversation : this.conversations) {
2988                    if (quickLoad != conversation) {
2989                        restoreMessages(conversation);
2990                    }
2991                }
2992                mNotificationService.finishBacklog();
2993                restoredFromDatabaseLatch.countDown();
2994                final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
2995                Log.d(Config.LOGTAG, "finished restoring messages in " + diffMessageRestore + "ms");
2996                updateConversationUi();
2997            };
2998            mDatabaseReaderExecutor.execute(runnable); //will contain one write command (expiry) but that's fine
2999        }
3000    }
3001
3002    private void restoreMessages(Conversation conversation) {
3003        conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
3004        conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
3005        conversation.findMessagesAndCallsToNotify(mNotificationService::pushFromBacklog);
3006    }
3007
3008    public void loadPhoneContacts() {
3009        mContactMergerExecutor.execute(
3010                () -> {
3011                    final Map<Jid, JabberIdContact> contacts = JabberIdContact.load(this);
3012                    Log.d(Config.LOGTAG, "start merging phone contacts with roster");
3013                    for (final Account account : accounts) {
3014                        final List<Contact> withSystemAccounts =
3015                                account.getRoster().getWithSystemAccounts(JabberIdContact.class);
3016                        for (final JabberIdContact jidContact : contacts.values()) {
3017                            final Contact contact =
3018                                    account.getRoster().getContact(jidContact.getJid());
3019                            boolean needsCacheClean = contact.setPhoneContact(jidContact);
3020                            if (needsCacheClean) {
3021                                getAvatarService().clear(contact);
3022                            }
3023                            withSystemAccounts.remove(contact);
3024                        }
3025                        for (final Contact contact : withSystemAccounts) {
3026                            boolean needsCacheClean =
3027                                    contact.unsetPhoneContact(JabberIdContact.class);
3028                            if (needsCacheClean) {
3029                                getAvatarService().clear(contact);
3030                            }
3031                        }
3032                    }
3033                    Log.d(Config.LOGTAG, "finished merging phone contacts");
3034                    mShortcutService.refresh(
3035                            mInitialAddressbookSyncCompleted.compareAndSet(false, true));
3036                    updateRosterUi(UpdateRosterReason.PUSH);
3037                    mQuickConversationsService.considerSync();
3038                });
3039    }
3040
3041    public void syncRoster(final Account account) {
3042        mRosterSyncTaskManager.execute(account, () -> {
3043            unregisterPhoneAccounts(account);
3044            databaseBackend.writeRoster(account.getRoster());
3045            try { Thread.sleep(500); } catch (InterruptedException e) { }
3046        });
3047    }
3048
3049    public List<Conversation> getConversations() {
3050        return this.conversations;
3051    }
3052
3053    private void markFileDeleted(final File file) {
3054        synchronized (FILENAMES_TO_IGNORE_DELETION) {
3055            if (FILENAMES_TO_IGNORE_DELETION.remove(file.getAbsolutePath())) {
3056                Log.d(Config.LOGTAG, "ignored deletion of " + file.getAbsolutePath());
3057                return;
3058            }
3059        }
3060        final boolean isInternalFile = fileBackend.isInternalFile(file);
3061        final List<String> uuids = databaseBackend.markFileAsDeleted(file, isInternalFile);
3062        Log.d(
3063                Config.LOGTAG,
3064                "deleted file "
3065                        + file.getAbsolutePath()
3066                        + " internal="
3067                        + isInternalFile
3068                        + ", database hits="
3069                        + uuids.size());
3070        markUuidsAsDeletedFiles(uuids);
3071    }
3072
3073    private void markUuidsAsDeletedFiles(List<String> uuids) {
3074        boolean deleted = false;
3075        for (Conversation conversation : getConversations()) {
3076            deleted |= conversation.markAsDeleted(uuids);
3077        }
3078        for (final String uuid : uuids) {
3079            evictPreview(uuid);
3080        }
3081        if (deleted) {
3082            updateConversationUi();
3083        }
3084    }
3085
3086    private void markChangedFiles(List<DatabaseBackend.FilePathInfo> infos) {
3087        boolean changed = false;
3088        for (Conversation conversation : getConversations()) {
3089            changed |= conversation.markAsChanged(infos);
3090        }
3091        if (changed) {
3092            updateConversationUi();
3093        }
3094    }
3095
3096    public void populateWithOrderedConversations(final List<Conversation> list) {
3097        populateWithOrderedConversations(list, true, true);
3098    }
3099
3100    public void populateWithOrderedConversations(
3101            final List<Conversation> list, final boolean includeNoFileUpload) {
3102        populateWithOrderedConversations(list, includeNoFileUpload, true);
3103    }
3104
3105    public void populateWithOrderedConversations(
3106            final List<Conversation> list, final boolean includeNoFileUpload, final boolean sort) {
3107        final List<String> orderedUuids;
3108        if (sort) {
3109            orderedUuids = null;
3110        } else {
3111            orderedUuids = new ArrayList<>();
3112            for (Conversation conversation : list) {
3113                orderedUuids.add(conversation.getUuid());
3114            }
3115        }
3116        list.clear();
3117        if (includeNoFileUpload) {
3118            list.addAll(getConversations());
3119        } else {
3120            for (Conversation conversation : getConversations()) {
3121                if (conversation.getMode() == Conversation.MODE_SINGLE
3122                        || (conversation.getAccount().httpUploadAvailable()
3123                                && conversation.getMucOptions().participating())) {
3124                    list.add(conversation);
3125                }
3126            }
3127        }
3128        try {
3129            if (orderedUuids != null) {
3130                Collections.sort(
3131                        list,
3132                        (a, b) -> {
3133                            final int indexA = orderedUuids.indexOf(a.getUuid());
3134                            final int indexB = orderedUuids.indexOf(b.getUuid());
3135                            if (indexA == -1 || indexB == -1 || indexA == indexB) {
3136                                return a.compareTo(b);
3137                            }
3138                            return indexA - indexB;
3139                        });
3140            } else {
3141                Collections.sort(list);
3142            }
3143        } catch (IllegalArgumentException e) {
3144            // ignore
3145        }
3146    }
3147
3148    public void loadMoreMessages(
3149            final Conversation conversation,
3150            final long timestamp,
3151            final OnMoreMessagesLoaded callback) {
3152        if (XmppConnectionService.this
3153                .getMessageArchiveService()
3154                .queryInProgress(conversation, callback)) {
3155            return;
3156        } else if (timestamp == 0) {
3157            return;
3158        }
3159        Log.d(
3160                Config.LOGTAG,
3161                "load more messages for "
3162                        + conversation.getName()
3163                        + " prior to "
3164                        + MessageGenerator.getTimestamp(timestamp));
3165        final Runnable runnable =
3166                () -> {
3167                    final Account account = conversation.getAccount();
3168                    List<Message> messages =
3169                            databaseBackend.getMessages(conversation, 50, timestamp);
3170                    if (messages.size() > 0) {
3171                        conversation.addAll(0, messages);
3172                        callback.onMoreMessagesLoaded(messages.size(), conversation);
3173                    } else if (conversation.hasMessagesLeftOnServer()
3174                            && account.isOnlineAndConnected()
3175                            && conversation.getLastClearHistory().getTimestamp() == 0) {
3176                        final boolean mamAvailable;
3177                        if (conversation.getMode() == Conversation.MODE_SINGLE) {
3178                            mamAvailable =
3179                                    account.getXmppConnection().getFeatures().mam()
3180                                            && !conversation.getContact().isBlocked();
3181                        } else {
3182                            mamAvailable = conversation.getMucOptions().mamSupport();
3183                        }
3184                        if (mamAvailable) {
3185                            MessageArchiveService.Query query =
3186                                    getMessageArchiveService()
3187                                            .query(
3188                                                    conversation,
3189                                                    new MamReference(0),
3190                                                    timestamp,
3191                                                    false);
3192                            if (query != null) {
3193                                query.setCallback(callback);
3194                                callback.informUser(R.string.fetching_history_from_server);
3195                            } else {
3196                                callback.informUser(R.string.not_fetching_history_retention_period);
3197                            }
3198                        }
3199                    }
3200                };
3201        mDatabaseReaderExecutor.execute(runnable);
3202    }
3203
3204    public List<Account> getAccounts() {
3205        return this.accounts;
3206    }
3207
3208    /**
3209     * This will find all conferences with the contact as member and also the conference that is the
3210     * contact (that 'fake' contact is used to store the avatar)
3211     */
3212    public List<Conversation> findAllConferencesWith(Contact contact) {
3213        final ArrayList<Conversation> results = new ArrayList<>();
3214        for (final Conversation c : conversations) {
3215            if (c.getMode() != Conversation.MODE_MULTI) {
3216                continue;
3217            }
3218            final MucOptions mucOptions = c.getMucOptions();
3219            if (c.getJid().asBareJid().equals(contact.getJid().asBareJid())
3220                    || (mucOptions != null && mucOptions.isContactInRoom(contact))) {
3221                results.add(c);
3222            }
3223        }
3224        return results;
3225    }
3226
3227    public Conversation find(final Contact contact) {
3228        for (final Conversation conversation : this.conversations) {
3229            if (conversation.getContact() == contact) {
3230                return conversation;
3231            }
3232        }
3233        return null;
3234    }
3235
3236    public Conversation find(
3237            final Iterable<Conversation> haystack, final Account account, final Jid jid) {
3238        if (jid == null) {
3239            return null;
3240        }
3241        for (final Conversation conversation : haystack) {
3242            if ((account == null || conversation.getAccount() == account)
3243                    && (conversation.getJid().asBareJid().equals(jid.asBareJid()))) {
3244                return conversation;
3245            }
3246        }
3247        return null;
3248    }
3249
3250    public boolean isConversationsListEmpty(final Conversation ignore) {
3251        synchronized (this.conversations) {
3252            final int size = this.conversations.size();
3253            return size == 0 || size == 1 && this.conversations.get(0) == ignore;
3254        }
3255    }
3256
3257    public boolean isConversationStillOpen(final Conversation conversation) {
3258        synchronized (this.conversations) {
3259            for (Conversation current : this.conversations) {
3260                if (current == conversation) {
3261                    return true;
3262                }
3263            }
3264        }
3265        return false;
3266    }
3267
3268    public void maybeRegisterWithMuc(Conversation c, String nickArg) {
3269        final var nick = nickArg == null ? c.getMucOptions().getSelf().getFullJid().getResource() : nickArg;
3270        final var register = new Iq(Iq.Type.GET);
3271        register.query(Namespace.REGISTER);
3272        register.setTo(c.getJid().asBareJid());
3273        sendIqPacket(c.getAccount(), register, (response) -> {
3274            if (response.getType() == Iq.Type.RESULT) {
3275                final Element query = response.query(Namespace.REGISTER);
3276                String username = query.findChildContent("username", Namespace.REGISTER);
3277                if (username == null) username = query.findChildContent("nick", Namespace.REGISTER);
3278                if (username != null && username.equals(nick)) {
3279                    // Already registered with this nick, done
3280                    Log.d(Config.LOGTAG, "Already registered with " + c.getJid().asBareJid() + " as " + username);
3281                    return;
3282                }
3283                Data form = Data.parse(query.findChild("x", Namespace.DATA));
3284                if (form != null) {
3285                    final var field = form.getFieldByName("muc#register_roomnick");
3286                    if (field != null && nick.equals(field.getValue())) {
3287                        Log.d(Config.LOGTAG, "Already registered with " + c.getJid().asBareJid() + " as " + field.getValue());
3288                        return;
3289                    }
3290                }
3291                if (form == null || !"form".equals(form.getFormType()) || !form.getFields().stream().anyMatch(f -> f.isRequired() && !"muc#register_roomnick".equals(f.getFieldName()))) {
3292                    // No form, result form, or no required fields other than nickname, let's just send nickname
3293                    if (form == null || !"form".equals(form.getFormType())) {
3294                        form = new Data();
3295                        form.put("FORM_TYPE", "http://jabber.org/protocol/muc#register");
3296                    }
3297                    form.put("muc#register_roomnick", nick);
3298                    form.submit();
3299                    final var finish = new Iq(Iq.Type.SET);
3300                    finish.query(Namespace.REGISTER).addChild(form);
3301                    finish.setTo(c.getJid().asBareJid());
3302                    sendIqPacket(c.getAccount(), finish, (response2) -> {
3303                        if (response.getType() == Iq.Type.RESULT) {
3304                            Log.w(Config.LOGTAG, "Success registering with channel " + c.getJid().asBareJid() + "/" + nick);
3305                        } else {
3306                            Log.w(Config.LOGTAG, "Error registering with channel: " + response2);
3307                        }
3308                    });
3309                } else {
3310                    // TODO: offer registration form to user
3311                    Log.d(Config.LOGTAG, "Complex registration form for " + c.getJid().asBareJid() + ": " + response);
3312                }
3313            } else {
3314                // We said maybe. Guess not
3315                Log.d(Config.LOGTAG, "Could not register with " + c.getJid().asBareJid() + ": " + response);
3316            }
3317        });
3318    }
3319
3320    public void deregisterWithMuc(Conversation c) {
3321        final Iq register = new Iq(Iq.Type.GET);
3322        register.query(Namespace.REGISTER).addChild("remove");
3323        register.setTo(c.getJid().asBareJid());
3324        sendIqPacket(c.getAccount(), register, (response) -> {
3325            if (response.getType() == Iq.Type.RESULT) {
3326                Log.d(Config.LOGTAG, "deregistered with " + c.getJid().asBareJid());
3327            } else {
3328                Log.w(Config.LOGTAG, "Could not deregister with " + c.getJid().asBareJid() + ": " + response);
3329            }
3330        });
3331    }
3332
3333    public Conversation findOrCreateConversation(
3334            Account account, Jid jid, boolean muc, final boolean async) {
3335        return this.findOrCreateConversation(account, jid, muc, false, async);
3336    }
3337
3338    public Conversation findOrCreateConversation(
3339            final Account account,
3340            final Jid jid,
3341            final boolean muc,
3342            final boolean joinAfterCreate,
3343            final boolean async) {
3344        return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, null, async, null);
3345    }
3346
3347    public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final MessageArchiveService.Query query, final boolean async) {
3348        return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, query, async, null);
3349    }
3350
3351    public Conversation findOrCreateConversation(
3352            final Account account,
3353            final Jid jid,
3354            final boolean muc,
3355            final boolean joinAfterCreate,
3356            final MessageArchiveService.Query query,
3357            final boolean async,
3358            final String password) {
3359        synchronized (this.conversations) {
3360            final var cached = find(account, jid);
3361            if (cached != null) {
3362                return cached;
3363            }
3364            final var existing = databaseBackend.findConversation(account, jid);
3365            final Conversation conversation;
3366            final boolean loadMessagesFromDb;
3367            if (existing != null) {
3368                conversation = existing;
3369                if (password != null) conversation.getMucOptions().setPassword(password);
3370                loadMessagesFromDb = restoreFromArchive(conversation, jid, muc);
3371            } else {
3372                String conversationName;
3373                final Contact contact = account.getRoster().getContact(jid);
3374                if (contact != null) {
3375                    conversationName = contact.getDisplayName();
3376                } else {
3377                    conversationName = jid.getLocal();
3378                }
3379                if (muc) {
3380                    conversation =
3381                            new Conversation(
3382                                    conversationName, account, jid, Conversation.MODE_MULTI);
3383                } else {
3384                    conversation =
3385                            new Conversation(
3386                                    conversationName,
3387                                    account,
3388                                    jid.asBareJid(),
3389                                    Conversation.MODE_SINGLE);
3390                }
3391                if (password != null) conversation.getMucOptions().setPassword(password);
3392                this.databaseBackend.createConversation(conversation);
3393                loadMessagesFromDb = false;
3394            }
3395            if (async) {
3396                mDatabaseReaderExecutor.execute(
3397                        () ->
3398                                postProcessConversation(
3399                                        conversation, loadMessagesFromDb, joinAfterCreate, query));
3400            } else {
3401                postProcessConversation(conversation, loadMessagesFromDb, joinAfterCreate, query);
3402            }
3403            this.conversations.add(conversation);
3404            updateConversationUi();
3405            return conversation;
3406        }
3407    }
3408
3409    public Conversation findConversationByUuidReliable(final String uuid) {
3410        final var cached = findConversationByUuid(uuid);
3411        if (cached != null) {
3412            return cached;
3413        }
3414        final var existing = databaseBackend.findConversation(uuid);
3415        if (existing == null) {
3416            return null;
3417        }
3418        Log.d(Config.LOGTAG, "restoring conversation with " + existing.getJid() + " from DB");
3419        final Map<String, Account> accounts =
3420                ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
3421        final var account = accounts.get(existing.getAccountUuid());
3422        if (account == null) {
3423            Log.d(Config.LOGTAG, "could not find account " + existing.getAccountUuid());
3424            return null;
3425        }
3426        existing.setAccount(account);
3427        final var loadMessagesFromDb = restoreFromArchive(existing);
3428        mDatabaseReaderExecutor.execute(
3429                () ->
3430                        postProcessConversation(
3431                                existing,
3432                                loadMessagesFromDb,
3433                                existing.getMode() == Conversational.MODE_MULTI,
3434                                null));
3435        this.conversations.add(existing);
3436        if (existing.getMode() == Conversational.MODE_MULTI) {
3437            ensureBookmarkIsAutoJoin(existing);
3438        }
3439        updateConversationUi();
3440        return existing;
3441    }
3442
3443    private boolean restoreFromArchive(
3444            final Conversation conversation, final Jid jid, final boolean muc) {
3445        if (muc) {
3446            conversation.setMode(Conversation.MODE_MULTI);
3447            conversation.setContactJid(jid);
3448        } else {
3449            conversation.setMode(Conversation.MODE_SINGLE);
3450            conversation.setContactJid(jid.asBareJid());
3451        }
3452        return restoreFromArchive(conversation);
3453    }
3454
3455    private boolean restoreFromArchive(final Conversation conversation) {
3456        conversation.setStatus(Conversation.STATUS_AVAILABLE);
3457        databaseBackend.updateConversation(conversation);
3458        return conversation.messagesLoaded.compareAndSet(true, false);
3459    }
3460
3461    private void postProcessConversation(
3462            final Conversation c,
3463            final boolean loadMessagesFromDb,
3464            final boolean joinAfterCreate,
3465            final MessageArchiveService.Query query) {
3466        final var singleMode = c.getMode() == Conversational.MODE_SINGLE;
3467        final var account = c.getAccount();
3468        if (loadMessagesFromDb) {
3469            c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE));
3470            updateConversationUi();
3471            c.messagesLoaded.set(true);
3472        }
3473        if (account.getXmppConnection() != null
3474                && !c.getContact().isBlocked()
3475                && account.getXmppConnection().getFeatures().mam()
3476                && singleMode) {
3477            if (query == null) {
3478                mMessageArchiveService.query(c);
3479            } else {
3480                if (query.getConversation() == null) {
3481                    mMessageArchiveService.query(c, query.getStart(), query.isCatchup());
3482                }
3483            }
3484        }
3485        if (joinAfterCreate) {
3486            joinMuc(c);
3487        }
3488    }
3489
3490    public void archiveConversation(Conversation conversation) {
3491        archiveConversation(conversation, true);
3492    }
3493
3494    private void archiveConversation(
3495            Conversation conversation, final boolean maySynchronizeWithBookmarks) {
3496        if (isOnboarding()) return;
3497
3498        getNotificationService().clear(conversation);
3499        conversation.setStatus(Conversation.STATUS_ARCHIVED);
3500        conversation.setNextMessage(null);
3501        synchronized (this.conversations) {
3502            getMessageArchiveService().kill(conversation);
3503            if (conversation.getMode() == Conversation.MODE_MULTI) {
3504                if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
3505                    final Bookmark bookmark = conversation.getBookmark();
3506                    if (maySynchronizeWithBookmarks && bookmark != null) {
3507                        if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) {
3508                            Account account = bookmark.getAccount();
3509                            bookmark.setConversation(null);
3510                            deleteBookmark(account, bookmark);
3511                        } else if (bookmark.autojoin()) {
3512                            bookmark.setAutojoin(false);
3513                            createBookmark(bookmark.getAccount(), bookmark);
3514                        }
3515                    }
3516                }
3517                deregisterWithMuc(conversation);
3518                leaveMuc(conversation);
3519            } else {
3520                if (conversation
3521                        .getContact()
3522                        .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
3523                    stopPresenceUpdatesTo(conversation.getContact());
3524                }
3525            }
3526            updateConversation(conversation);
3527            this.conversations.remove(conversation);
3528            updateConversationUi();
3529        }
3530    }
3531
3532    public void stopPresenceUpdatesTo(Contact contact) {
3533        Log.d(Config.LOGTAG, "Canceling presence request from " + contact.getJid().toString());
3534        sendPresencePacket(contact.getAccount(), mPresenceGenerator.stopPresenceUpdatesTo(contact));
3535        contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
3536    }
3537
3538    public void createAccount(final Account account) {
3539        account.initAccountServices(this);
3540        databaseBackend.createAccount(account);
3541        if (CallIntegration.hasSystemFeature(this)) {
3542            CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
3543        }
3544        this.accounts.add(account);
3545        this.reconnectAccountInBackground(account);
3546        updateAccountUi();
3547        syncEnabledAccountSetting();
3548        toggleForegroundService();
3549    }
3550
3551    private void syncEnabledAccountSetting() {
3552        final boolean hasEnabledAccounts = hasEnabledAccounts();
3553        getPreferences()
3554                .edit()
3555                .putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts)
3556                .apply();
3557        toggleSetProfilePictureActivity(hasEnabledAccounts);
3558    }
3559
3560    private void toggleSetProfilePictureActivity(final boolean enabled) {
3561        try {
3562            final ComponentName name =
3563                    new ComponentName(this, ChooseAccountForProfilePictureActivity.class);
3564            final int targetState =
3565                    enabled
3566                            ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
3567                            : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
3568            getPackageManager()
3569                    .setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP);
3570        } catch (IllegalStateException e) {
3571            Log.d(Config.LOGTAG, "unable to toggle profile picture activity");
3572        }
3573    }
3574
3575    public boolean reconfigurePushDistributor() {
3576        return this.unifiedPushBroker.reconfigurePushDistributor();
3577    }
3578
3579    private Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints(
3580            final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger) {
3581        return this.unifiedPushBroker.renewUnifiedPushEndpoints(pushTargetMessenger);
3582    }
3583
3584    public Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints() {
3585        return this.unifiedPushBroker.renewUnifiedPushEndpoints(null);
3586    }
3587
3588    public UnifiedPushBroker getUnifiedPushBroker() {
3589        return this.unifiedPushBroker;
3590    }
3591
3592    private void provisionAccount(final String address, final String password) {
3593        final Jid jid = Jid.of(address);
3594        final Account account = new Account(jid, password);
3595        account.setOption(Account.OPTION_DISABLED, true);
3596        Log.d(Config.LOGTAG, jid.asBareJid().toString() + ": provisioning account");
3597        createAccount(account);
3598    }
3599
3600    public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
3601        new Thread(
3602                        () -> {
3603                            try {
3604                                final X509Certificate[] chain =
3605                                        KeyChain.getCertificateChain(this, alias);
3606                                final X509Certificate cert =
3607                                        chain != null && chain.length > 0 ? chain[0] : null;
3608                                if (cert == null) {
3609                                    callback.informUser(R.string.unable_to_parse_certificate);
3610                                    return;
3611                                }
3612                                Pair<Jid, String> info = CryptoHelper.extractJidAndName(cert);
3613                                if (info == null) {
3614                                    callback.informUser(R.string.certificate_does_not_contain_jid);
3615                                    return;
3616                                }
3617                                if (findAccountByJid(info.first) == null) {
3618                                    final Account account = new Account(info.first, "");
3619                                    account.setPrivateKeyAlias(alias);
3620                                    account.setOption(Account.OPTION_DISABLED, true);
3621                                    account.setOption(Account.OPTION_FIXED_USERNAME, true);
3622                                    account.setDisplayName(info.second);
3623                                    createAccount(account);
3624                                    callback.onAccountCreated(account);
3625                                    if (Config.X509_VERIFICATION) {
3626                                        try {
3627                                            getMemorizingTrustManager()
3628                                                    .getNonInteractive(account.getServer(), null, 0, null)
3629                                                    .checkClientTrusted(chain, "RSA");
3630                                        } catch (CertificateException e) {
3631                                            callback.informUser(
3632                                                    R.string.certificate_chain_is_not_trusted);
3633                                        }
3634                                    }
3635                                } else {
3636                                    callback.informUser(R.string.account_already_exists);
3637                                }
3638                            } catch (Exception e) {
3639                                callback.informUser(R.string.unable_to_parse_certificate);
3640                            }
3641                        })
3642                .start();
3643    }
3644
3645    public void updateKeyInAccount(final Account account, final String alias) {
3646        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": update key in account " + alias);
3647        try {
3648            X509Certificate[] chain =
3649                    KeyChain.getCertificateChain(XmppConnectionService.this, alias);
3650            Log.d(Config.LOGTAG, account.getJid().asBareJid() + " loaded certificate chain");
3651            Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
3652            if (info == null) {
3653                showErrorToastInUi(R.string.certificate_does_not_contain_jid);
3654                return;
3655            }
3656            if (account.getJid().asBareJid().equals(info.first)) {
3657                account.setPrivateKeyAlias(alias);
3658                account.setDisplayName(info.second);
3659                databaseBackend.updateAccount(account);
3660                if (Config.X509_VERIFICATION) {
3661                    try {
3662                        getMemorizingTrustManager()
3663                                .getNonInteractive()
3664                                .checkClientTrusted(chain, "RSA");
3665                    } catch (CertificateException e) {
3666                        showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
3667                    }
3668                    account.getAxolotlService().regenerateKeys(true);
3669                }
3670            } else {
3671                showErrorToastInUi(R.string.jid_does_not_match_certificate);
3672            }
3673        } catch (Exception e) {
3674            e.printStackTrace();
3675        }
3676    }
3677
3678    public boolean updateAccount(final Account account) {
3679        if (databaseBackend.updateAccount(account)) {
3680            Integer color = account.getColorToSave();
3681            if (color == null) {
3682                getPreferences().edit().remove("account_color:" + account.getUuid()).commit();
3683            } else {
3684                getPreferences().edit().putInt("account_color:" + account.getUuid(), color.intValue()).commit();
3685            }
3686            account.setShowErrorNotification(true);
3687            this.statusListener.onStatusChanged(account);
3688            databaseBackend.updateAccount(account);
3689            reconnectAccountInBackground(account);
3690            updateAccountUi();
3691            getNotificationService().updateErrorNotification();
3692            toggleForegroundService();
3693            syncEnabledAccountSetting();
3694            mChannelDiscoveryService.cleanCache();
3695            if (CallIntegration.hasSystemFeature(this)) {
3696                CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
3697            }
3698            return true;
3699        } else {
3700            return false;
3701        }
3702    }
3703
3704    public void updateAccountPasswordOnServer(
3705            final Account account,
3706            final String newPassword,
3707            final OnAccountPasswordChanged callback) {
3708        final Iq iq = getIqGenerator().generateSetPassword(account, newPassword);
3709        sendIqPacket(
3710                account,
3711                iq,
3712                (packet) -> {
3713                    if (packet.getType() == Iq.Type.RESULT) {
3714                        account.setPassword(newPassword);
3715                        account.setOption(Account.OPTION_MAGIC_CREATE, false);
3716                        databaseBackend.updateAccount(account);
3717                        callback.onPasswordChangeSucceeded();
3718                    } else {
3719                        callback.onPasswordChangeFailed();
3720                    }
3721                });
3722    }
3723
3724    public void unregisterAccount(final Account account, final Consumer<Boolean> callback) {
3725        final Iq iqPacket = new Iq(Iq.Type.SET);
3726        final Element query = iqPacket.addChild("query", Namespace.REGISTER);
3727        query.addChild("remove");
3728        sendIqPacket(
3729                account,
3730                iqPacket,
3731                (response) -> {
3732                    if (response.getType() == Iq.Type.RESULT) {
3733                        deleteAccount(account);
3734                        callback.accept(true);
3735                    } else {
3736                        callback.accept(false);
3737                    }
3738                });
3739    }
3740
3741    public void deleteAccount(final Account account) {
3742        getPreferences().edit().remove("onboarding_continued").commit();
3743        final boolean connected = account.getStatus() == Account.State.ONLINE;
3744        synchronized (this.conversations) {
3745            if (connected) {
3746                account.getAxolotlService().deleteOmemoIdentity();
3747            }
3748            for (final Conversation conversation : conversations) {
3749                if (conversation.getAccount() == account) {
3750                    if (conversation.getMode() == Conversation.MODE_MULTI) {
3751                        if (connected) {
3752                            leaveMuc(conversation);
3753                        }
3754                    }
3755                    conversations.remove(conversation);
3756                    mNotificationService.clear(conversation);
3757                }
3758            }
3759            new Thread(() -> {
3760                for (final Contact contact : account.getRoster().getContacts()) {
3761                    contact.unregisterAsPhoneAccount(this);
3762                }
3763            }).start();
3764            if (account.getXmppConnection() != null) {
3765                new Thread(() -> disconnect(account, !connected)).start();
3766            }
3767            final Runnable runnable =
3768                    () -> {
3769                        if (!databaseBackend.deleteAccount(account)) {
3770                            Log.d(
3771                                    Config.LOGTAG,
3772                                    account.getJid().asBareJid() + ": unable to delete account");
3773                        }
3774                    };
3775            mDatabaseWriterExecutor.execute(runnable);
3776            this.accounts.remove(account);
3777            if (CallIntegration.hasSystemFeature(this)) {
3778                CallIntegrationConnectionService.unregisterPhoneAccount(this, account);
3779            }
3780            this.mRosterSyncTaskManager.clear(account);
3781            updateAccountUi();
3782            mNotificationService.updateErrorNotification();
3783            syncEnabledAccountSetting();
3784            toggleForegroundService();
3785        }
3786    }
3787
3788    public void setOnConversationListChangedListener(OnConversationUpdate listener) {
3789        final boolean remainingListeners;
3790        synchronized (LISTENER_LOCK) {
3791            remainingListeners = checkListeners();
3792            if (!this.mOnConversationUpdates.add(listener)) {
3793                Log.w(
3794                        Config.LOGTAG,
3795                        listener.getClass().getName()
3796                                + " is already registered as ConversationListChangedListener");
3797            }
3798            this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
3799        }
3800        if (remainingListeners) {
3801            switchToForeground();
3802        }
3803    }
3804
3805    public void removeOnConversationListChangedListener(OnConversationUpdate listener) {
3806        final boolean remainingListeners;
3807        synchronized (LISTENER_LOCK) {
3808            this.mOnConversationUpdates.remove(listener);
3809            this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
3810            remainingListeners = checkListeners();
3811        }
3812        if (remainingListeners) {
3813            switchToBackground();
3814        }
3815    }
3816
3817    public void setOnShowErrorToastListener(OnShowErrorToast listener) {
3818        final boolean remainingListeners;
3819        synchronized (LISTENER_LOCK) {
3820            remainingListeners = checkListeners();
3821            if (!this.mOnShowErrorToasts.add(listener)) {
3822                Log.w(
3823                        Config.LOGTAG,
3824                        listener.getClass().getName()
3825                                + " is already registered as OnShowErrorToastListener");
3826            }
3827        }
3828        if (remainingListeners) {
3829            switchToForeground();
3830        }
3831    }
3832
3833    public void removeOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
3834        final boolean remainingListeners;
3835        synchronized (LISTENER_LOCK) {
3836            this.mOnShowErrorToasts.remove(onShowErrorToast);
3837            remainingListeners = checkListeners();
3838        }
3839        if (remainingListeners) {
3840            switchToBackground();
3841        }
3842    }
3843
3844    public void setOnAccountListChangedListener(OnAccountUpdate listener) {
3845        final boolean remainingListeners;
3846        synchronized (LISTENER_LOCK) {
3847            remainingListeners = checkListeners();
3848            if (!this.mOnAccountUpdates.add(listener)) {
3849                Log.w(
3850                        Config.LOGTAG,
3851                        listener.getClass().getName()
3852                                + " is already registered as OnAccountListChangedtListener");
3853            }
3854        }
3855        if (remainingListeners) {
3856            switchToForeground();
3857        }
3858    }
3859
3860    public void removeOnAccountListChangedListener(OnAccountUpdate listener) {
3861        final boolean remainingListeners;
3862        synchronized (LISTENER_LOCK) {
3863            this.mOnAccountUpdates.remove(listener);
3864            remainingListeners = checkListeners();
3865        }
3866        if (remainingListeners) {
3867            switchToBackground();
3868        }
3869    }
3870
3871    public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
3872        final boolean remainingListeners;
3873        synchronized (LISTENER_LOCK) {
3874            remainingListeners = checkListeners();
3875            if (!this.mOnCaptchaRequested.add(listener)) {
3876                Log.w(
3877                        Config.LOGTAG,
3878                        listener.getClass().getName()
3879                                + " is already registered as OnCaptchaRequestListener");
3880            }
3881        }
3882        if (remainingListeners) {
3883            switchToForeground();
3884        }
3885    }
3886
3887    public void removeOnCaptchaRequestedListener(OnCaptchaRequested listener) {
3888        final boolean remainingListeners;
3889        synchronized (LISTENER_LOCK) {
3890            this.mOnCaptchaRequested.remove(listener);
3891            remainingListeners = checkListeners();
3892        }
3893        if (remainingListeners) {
3894            switchToBackground();
3895        }
3896    }
3897
3898    public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
3899        final boolean remainingListeners;
3900        synchronized (LISTENER_LOCK) {
3901            remainingListeners = checkListeners();
3902            if (!this.mOnRosterUpdates.add(listener)) {
3903                Log.w(
3904                        Config.LOGTAG,
3905                        listener.getClass().getName()
3906                                + " is already registered as OnRosterUpdateListener");
3907            }
3908        }
3909        if (remainingListeners) {
3910            switchToForeground();
3911        }
3912    }
3913
3914    public void removeOnRosterUpdateListener(final OnRosterUpdate listener) {
3915        final boolean remainingListeners;
3916        synchronized (LISTENER_LOCK) {
3917            this.mOnRosterUpdates.remove(listener);
3918            remainingListeners = checkListeners();
3919        }
3920        if (remainingListeners) {
3921            switchToBackground();
3922        }
3923    }
3924
3925    public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
3926        final boolean remainingListeners;
3927        synchronized (LISTENER_LOCK) {
3928            remainingListeners = checkListeners();
3929            if (!this.mOnUpdateBlocklist.add(listener)) {
3930                Log.w(
3931                        Config.LOGTAG,
3932                        listener.getClass().getName()
3933                                + " is already registered as OnUpdateBlocklistListener");
3934            }
3935        }
3936        if (remainingListeners) {
3937            switchToForeground();
3938        }
3939    }
3940
3941    public void removeOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
3942        final boolean remainingListeners;
3943        synchronized (LISTENER_LOCK) {
3944            this.mOnUpdateBlocklist.remove(listener);
3945            remainingListeners = checkListeners();
3946        }
3947        if (remainingListeners) {
3948            switchToBackground();
3949        }
3950    }
3951
3952    public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
3953        final boolean remainingListeners;
3954        synchronized (LISTENER_LOCK) {
3955            remainingListeners = checkListeners();
3956            if (!this.mOnKeyStatusUpdated.add(listener)) {
3957                Log.w(
3958                        Config.LOGTAG,
3959                        listener.getClass().getName()
3960                                + " is already registered as OnKeyStatusUpdateListener");
3961            }
3962        }
3963        if (remainingListeners) {
3964            switchToForeground();
3965        }
3966    }
3967
3968    public void removeOnNewKeysAvailableListener(final OnKeyStatusUpdated listener) {
3969        final boolean remainingListeners;
3970        synchronized (LISTENER_LOCK) {
3971            this.mOnKeyStatusUpdated.remove(listener);
3972            remainingListeners = checkListeners();
3973        }
3974        if (remainingListeners) {
3975            switchToBackground();
3976        }
3977    }
3978
3979    public void setOnRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
3980        final boolean remainingListeners;
3981        synchronized (LISTENER_LOCK) {
3982            remainingListeners = checkListeners();
3983            if (!this.onJingleRtpConnectionUpdate.add(listener)) {
3984                Log.w(
3985                        Config.LOGTAG,
3986                        listener.getClass().getName()
3987                                + " is already registered as OnJingleRtpConnectionUpdate");
3988            }
3989        }
3990        if (remainingListeners) {
3991            switchToForeground();
3992        }
3993    }
3994
3995    public void removeRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
3996        final boolean remainingListeners;
3997        synchronized (LISTENER_LOCK) {
3998            this.onJingleRtpConnectionUpdate.remove(listener);
3999            remainingListeners = checkListeners();
4000        }
4001        if (remainingListeners) {
4002            switchToBackground();
4003        }
4004    }
4005
4006    public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
4007        final boolean remainingListeners;
4008        synchronized (LISTENER_LOCK) {
4009            remainingListeners = checkListeners();
4010            if (!this.mOnMucRosterUpdate.add(listener)) {
4011                Log.w(
4012                        Config.LOGTAG,
4013                        listener.getClass().getName()
4014                                + " is already registered as OnMucRosterListener");
4015            }
4016        }
4017        if (remainingListeners) {
4018            switchToForeground();
4019        }
4020    }
4021
4022    public void removeOnMucRosterUpdateListener(final OnMucRosterUpdate listener) {
4023        final boolean remainingListeners;
4024        synchronized (LISTENER_LOCK) {
4025            this.mOnMucRosterUpdate.remove(listener);
4026            remainingListeners = checkListeners();
4027        }
4028        if (remainingListeners) {
4029            switchToBackground();
4030        }
4031    }
4032
4033    public boolean checkListeners() {
4034        return (this.mOnAccountUpdates.isEmpty()
4035                && this.mOnConversationUpdates.isEmpty()
4036                && this.mOnRosterUpdates.isEmpty()
4037                && this.mOnCaptchaRequested.isEmpty()
4038                && this.mOnMucRosterUpdate.isEmpty()
4039                && this.mOnUpdateBlocklist.isEmpty()
4040                && this.mOnShowErrorToasts.isEmpty()
4041                && this.onJingleRtpConnectionUpdate.isEmpty()
4042                && this.mOnKeyStatusUpdated.isEmpty());
4043    }
4044
4045    private void switchToForeground() {
4046        toggleSoftDisabled(false);
4047        final boolean broadcastLastActivity = broadcastLastActivity();
4048        for (Conversation conversation : getConversations()) {
4049            if (conversation.getMode() == Conversation.MODE_MULTI) {
4050                conversation.getMucOptions().resetChatState();
4051            } else {
4052                conversation.setIncomingChatState(Config.DEFAULT_CHAT_STATE);
4053            }
4054        }
4055        for (Account account : getAccounts()) {
4056            if (account.getStatus() == Account.State.ONLINE) {
4057                account.deactivateGracePeriod();
4058                final XmppConnection connection = account.getXmppConnection();
4059                if (connection != null) {
4060                    if (connection.getFeatures().csi()) {
4061                        connection.sendActive();
4062                    }
4063                    if (broadcastLastActivity) {
4064                        sendPresence(
4065                                account,
4066                                false); // send new presence but don't include idle because we are
4067                        // not
4068                    }
4069                }
4070            }
4071        }
4072        Log.d(Config.LOGTAG, "app switched into foreground");
4073    }
4074
4075    private void switchToBackground() {
4076        final boolean broadcastLastActivity = broadcastLastActivity();
4077        if (broadcastLastActivity) {
4078            mLastActivity = System.currentTimeMillis();
4079            final SharedPreferences.Editor editor = getPreferences().edit();
4080            editor.putLong(SETTING_LAST_ACTIVITY_TS, mLastActivity);
4081            editor.apply();
4082        }
4083        for (Account account : getAccounts()) {
4084            if (account.getStatus() == Account.State.ONLINE) {
4085                XmppConnection connection = account.getXmppConnection();
4086                if (connection != null) {
4087                    if (broadcastLastActivity) {
4088                        sendPresence(account, true);
4089                    }
4090                    if (connection.getFeatures().csi()) {
4091                        connection.sendInactive();
4092                    }
4093                }
4094            }
4095        }
4096        this.mNotificationService.setIsInForeground(false);
4097        Log.d(Config.LOGTAG, "app switched into background");
4098    }
4099
4100    public void connectMultiModeConversations(Account account) {
4101        List<Conversation> conversations = getConversations();
4102        for (Conversation conversation : conversations) {
4103            if (conversation.getMode() == Conversation.MODE_MULTI
4104                    && conversation.getAccount() == account) {
4105                joinMuc(conversation);
4106            }
4107        }
4108    }
4109
4110    public void mucSelfPingAndRejoin(final Conversation conversation) {
4111        final Account account = conversation.getAccount();
4112        synchronized (account.inProgressConferenceJoins) {
4113            if (account.inProgressConferenceJoins.contains(conversation)) {
4114                Log.d(
4115                        Config.LOGTAG,
4116                        account.getJid().asBareJid()
4117                                + ": canceling muc self ping because join is already under way");
4118                return;
4119            }
4120        }
4121        synchronized (account.inProgressConferencePings) {
4122            if (!account.inProgressConferencePings.add(conversation)) {
4123                Log.d(
4124                        Config.LOGTAG,
4125                        account.getJid().asBareJid()
4126                                + ": canceling muc self ping because ping is already under way");
4127                return;
4128            }
4129        }
4130        final Jid self = conversation.getMucOptions().getSelf().getFullJid();
4131        final Iq ping = new Iq(Iq.Type.GET);
4132        ping.setTo(self);
4133        ping.addChild("ping", Namespace.PING);
4134        sendIqPacket(
4135                conversation.getAccount(),
4136                ping,
4137                (response) -> {
4138                    if (response.getType() == Iq.Type.ERROR) {
4139                        final var error = response.getError();
4140                        if (error == null
4141                                || error.hasChild("service-unavailable")
4142                                || error.hasChild("feature-not-implemented")
4143                                || error.hasChild("item-not-found")) {
4144                            Log.d(
4145                                    Config.LOGTAG,
4146                                    account.getJid().asBareJid()
4147                                            + ": ping to "
4148                                            + self
4149                                            + " came back as ignorable error");
4150                        } else {
4151                            Log.d(
4152                                    Config.LOGTAG,
4153                                    account.getJid().asBareJid()
4154                                            + ": ping to "
4155                                            + self
4156                                            + " failed. attempting rejoin");
4157                            joinMuc(conversation);
4158                        }
4159                    } else if (response.getType() == Iq.Type.RESULT) {
4160                        Log.d(
4161                                Config.LOGTAG,
4162                                account.getJid().asBareJid()
4163                                        + ": ping to "
4164                                        + self
4165                                        + " came back fine");
4166                    }
4167                    synchronized (account.inProgressConferencePings) {
4168                        account.inProgressConferencePings.remove(conversation);
4169                    }
4170                });
4171    }
4172
4173    public void joinMuc(Conversation conversation) {
4174        joinMuc(conversation, null, false);
4175    }
4176
4177    public void joinMuc(Conversation conversation, boolean followedInvite) {
4178        joinMuc(conversation, null, followedInvite);
4179    }
4180
4181    private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
4182        joinMuc(conversation, onConferenceJoined, false);
4183    }
4184
4185    private void joinMuc(
4186            Conversation conversation,
4187            final OnConferenceJoined onConferenceJoined,
4188            final boolean followedInvite) {
4189        final Account account = conversation.getAccount();
4190        synchronized (account.pendingConferenceJoins) {
4191            account.pendingConferenceJoins.remove(conversation);
4192        }
4193        synchronized (account.pendingConferenceLeaves) {
4194            account.pendingConferenceLeaves.remove(conversation);
4195        }
4196        if (account.getStatus() == Account.State.ONLINE) {
4197            synchronized (account.inProgressConferenceJoins) {
4198                account.inProgressConferenceJoins.add(conversation);
4199            }
4200            if (Config.MUC_LEAVE_BEFORE_JOIN) {
4201                sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions()));
4202            }
4203            conversation.resetMucOptions();
4204            if (onConferenceJoined != null) {
4205                conversation.getMucOptions().flagNoAutoPushConfiguration();
4206            }
4207            conversation.setHasMessagesLeftOnServer(false);
4208            fetchConferenceConfiguration(
4209                    conversation,
4210                    new OnConferenceConfigurationFetched() {
4211
4212                        private void join(Conversation conversation) {
4213                            Account account = conversation.getAccount();
4214                            final MucOptions mucOptions = conversation.getMucOptions();
4215
4216                            if (mucOptions.nonanonymous()
4217                                    && !mucOptions.membersOnly()
4218                                    && !conversation.getBooleanAttribute(
4219                                            "accept_non_anonymous", false)) {
4220                                synchronized (account.inProgressConferenceJoins) {
4221                                    account.inProgressConferenceJoins.remove(conversation);
4222                                }
4223                                mucOptions.setError(MucOptions.Error.NON_ANONYMOUS);
4224                                updateConversationUi();
4225                                if (onConferenceJoined != null) {
4226                                    onConferenceJoined.onConferenceJoined(conversation);
4227                                }
4228                                return;
4229                            }
4230
4231                            final Jid joinJid = mucOptions.getSelf().getFullJid();
4232                            Log.d(
4233                                    Config.LOGTAG,
4234                                    account.getJid().asBareJid().toString()
4235                                            + ": joining conversation "
4236                                            + joinJid.toString());
4237                            final var packet =
4238                                    mPresenceGenerator.selfPresence(
4239                                            account,
4240                                            Presence.Status.ONLINE,
4241                                            mucOptions.nonanonymous()
4242                                                    || onConferenceJoined != null,
4243                                            mucOptions.getSelf().getNick());
4244                            packet.setTo(joinJid);
4245                            Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
4246                            if (conversation.getMucOptions().getPassword() != null) {
4247                                x.addChild("password").setContent(mucOptions.getPassword());
4248                            }
4249
4250                            if (mucOptions.mamSupport()) {
4251                                // Use MAM instead of the limited muc history to get history
4252                                x.addChild("history").setAttribute("maxchars", "0");
4253                            } else {
4254                                // Fallback to muc history
4255                                x.addChild("history")
4256                                        .setAttribute(
4257                                                "since",
4258                                                PresenceGenerator.getTimestamp(
4259                                                        conversation
4260                                                                .getLastMessageTransmitted()
4261                                                                .getTimestamp()));
4262                            }
4263                            sendPresencePacket(account, packet);
4264                            if (onConferenceJoined != null) {
4265                                onConferenceJoined.onConferenceJoined(conversation);
4266                            }
4267                            if (!joinJid.equals(conversation.getJid())) {
4268                                conversation.setContactJid(joinJid);
4269                                databaseBackend.updateConversation(conversation);
4270                            }
4271
4272                            maybeRegisterWithMuc(conversation, null);
4273
4274                            if (mucOptions.mamSupport()) {
4275                                getMessageArchiveService().catchupMUC(conversation);
4276                            }
4277                            fetchConferenceMembers(conversation);
4278                            if (mucOptions.isPrivateAndNonAnonymous()) {
4279                                if (followedInvite) {
4280                                    final Bookmark bookmark = conversation.getBookmark();
4281                                    if (bookmark != null) {
4282                                        if (!bookmark.autojoin()) {
4283                                            bookmark.setAutojoin(true);
4284                                            createBookmark(account, bookmark);
4285                                        }
4286                                    } else {
4287                                        saveConversationAsBookmark(conversation, null);
4288                                    }
4289                                }
4290                            }
4291                            synchronized (account.inProgressConferenceJoins) {
4292                                account.inProgressConferenceJoins.remove(conversation);
4293                                sendUnsentMessages(conversation);
4294                            }
4295                        }
4296
4297                        @Override
4298                        public void onConferenceConfigurationFetched(Conversation conversation) {
4299                            if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
4300                                Log.d(
4301                                        Config.LOGTAG,
4302                                        account.getJid().asBareJid()
4303                                                + ": conversation ("
4304                                                + conversation.getJid()
4305                                                + ") got archived before IQ result");
4306                                return;
4307                            }
4308                            join(conversation);
4309                        }
4310
4311                        @Override
4312                        public void onFetchFailed(
4313                                final Conversation conversation, final String errorCondition) {
4314                            if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
4315                                Log.d(
4316                                        Config.LOGTAG,
4317                                        account.getJid().asBareJid()
4318                                                + ": conversation ("
4319                                                + conversation.getJid()
4320                                                + ") got archived before IQ result");
4321                                return;
4322                            }
4323                            if ("remote-server-not-found".equals(errorCondition)) {
4324                                synchronized (account.inProgressConferenceJoins) {
4325                                    account.inProgressConferenceJoins.remove(conversation);
4326                                }
4327                                conversation
4328                                        .getMucOptions()
4329                                        .setError(MucOptions.Error.SERVER_NOT_FOUND);
4330                                updateConversationUi();
4331                            } else {
4332                                join(conversation);
4333                                fetchConferenceConfiguration(conversation);
4334                            }
4335                        }
4336                    });
4337            updateConversationUi();
4338        } else {
4339            synchronized (account.pendingConferenceJoins) {
4340                account.pendingConferenceJoins.add(conversation);
4341            }
4342            conversation.resetMucOptions();
4343            conversation.setHasMessagesLeftOnServer(false);
4344            updateConversationUi();
4345        }
4346    }
4347
4348    private void fetchConferenceMembers(final Conversation conversation) {
4349        final Account account = conversation.getAccount();
4350        final AxolotlService axolotlService = account.getAxolotlService();
4351        final var affiliations = new ArrayList<String>();
4352        affiliations.add("outcast");
4353        if (conversation.getMucOptions().isPrivateAndNonAnonymous()) affiliations.addAll(List.of("member", "admin", "owner"));
4354        final Consumer<Iq> callback =
4355                new Consumer<Iq>() {
4356
4357                    private int i = 0;
4358                    private boolean success = true;
4359
4360                    @Override
4361                    public void accept(Iq response) {
4362                        final boolean omemoEnabled =
4363                                conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL;
4364                        Element query = response.query("http://jabber.org/protocol/muc#admin");
4365                        if (response.getType() == Iq.Type.RESULT && query != null) {
4366                            for (Element child : query.getChildren()) {
4367                                if ("item".equals(child.getName())) {
4368                                    MucOptions.User user =
4369                                            AbstractParser.parseItem(conversation, child);
4370                                    user.setOnline(false);
4371                                    if (!user.realJidMatchesAccount()) {
4372                                        boolean isNew =
4373                                                conversation.getMucOptions().updateUser(user);
4374                                        Contact contact = user.getContact();
4375                                        if (omemoEnabled
4376                                                && isNew
4377                                                && user.getRealJid() != null
4378                                                && (contact == null
4379                                                        || !contact.mutualPresenceSubscription())
4380                                                && axolotlService.hasEmptyDeviceList(
4381                                                        user.getRealJid())) {
4382                                            axolotlService.fetchDeviceIds(user.getRealJid());
4383                                        }
4384                                    }
4385                                }
4386                            }
4387                        } else {
4388                            success = false;
4389                            Log.d(
4390                                    Config.LOGTAG,
4391                                    account.getJid().asBareJid()
4392                                            + ": could not request affiliation "
4393                                            + affiliations.get(i)
4394                                            + " in "
4395                                            + conversation.getJid().asBareJid());
4396                        }
4397                        ++i;
4398                        if (i >= affiliations.size()) {
4399                            final var mucOptions = conversation.getMucOptions();
4400                            List<Jid> members = mucOptions.getMembers(true);
4401                            if (success) {
4402                                List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
4403                                boolean changed = false;
4404                                for (ListIterator<Jid> iterator = cryptoTargets.listIterator();
4405                                        iterator.hasNext(); ) {
4406                                    Jid jid = iterator.next();
4407                                    if (!members.contains(jid)
4408                                            && !members.contains(jid.getDomain())) {
4409                                        iterator.remove();
4410                                        Log.d(
4411                                                Config.LOGTAG,
4412                                                account.getJid().asBareJid()
4413                                                        + ": removed "
4414                                                        + jid
4415                                                        + " from crypto targets of "
4416                                                        + conversation.getName());
4417                                        changed = true;
4418                                    }
4419                                }
4420                                if (changed) {
4421                                    conversation.setAcceptedCryptoTargets(cryptoTargets);
4422                                    updateConversation(conversation);
4423                                }
4424                            }
4425                            getAvatarService().clear(mucOptions);
4426                            updateMucRosterUi();
4427                            updateConversationUi();
4428                        }
4429                    }
4430                };
4431        for (String affiliation : affiliations) {
4432            sendIqPacket(
4433                    account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
4434        }
4435        Log.d(
4436                Config.LOGTAG,
4437                account.getJid().asBareJid() + ": fetching members for " + conversation.getName());
4438    }
4439
4440    public void providePasswordForMuc(final Conversation conversation, final String password) {
4441        if (conversation.getMode() == Conversation.MODE_MULTI) {
4442            conversation.getMucOptions().setPassword(password);
4443            if (conversation.getBookmark() != null) {
4444                final Bookmark bookmark = conversation.getBookmark();
4445                bookmark.setAutojoin(true);
4446                createBookmark(conversation.getAccount(), bookmark);
4447            }
4448            updateConversation(conversation);
4449            joinMuc(conversation);
4450        }
4451    }
4452
4453    public void deleteAvatar(final Account account) {
4454        final AtomicBoolean executed = new AtomicBoolean(false);
4455        final Runnable onDeleted =
4456                () -> {
4457                    if (executed.compareAndSet(false, true)) {
4458                        account.setAvatar(null);
4459                        databaseBackend.updateAccount(account);
4460                        getAvatarService().clear(account);
4461                        updateAccountUi();
4462                    }
4463                };
4464        deleteVcardAvatar(account, onDeleted);
4465        deletePepNode(account, Namespace.AVATAR_DATA);
4466        deletePepNode(account, Namespace.AVATAR_METADATA, onDeleted);
4467    }
4468
4469    public void deletePepNode(final Account account, final String node) {
4470        deletePepNode(account, node, null);
4471    }
4472
4473    private void deletePepNode(final Account account, final String node, final Runnable runnable) {
4474        final Iq request = mIqGenerator.deleteNode(node);
4475        sendIqPacket(
4476                account,
4477                request,
4478                (packet) -> {
4479                    if (packet.getType() == Iq.Type.RESULT) {
4480                        Log.d(
4481                                Config.LOGTAG,
4482                                account.getJid().asBareJid()
4483                                        + ": successfully deleted pep node "
4484                                        + node);
4485                        if (runnable != null) {
4486                            runnable.run();
4487                        }
4488                    } else {
4489                        Log.d(
4490                                Config.LOGTAG,
4491                                account.getJid().asBareJid() + ": failed to delete " + packet);
4492                    }
4493                });
4494    }
4495
4496    private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) {
4497        final Iq retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid());
4498        sendIqPacket(
4499                account,
4500                retrieveVcard,
4501                (response) -> {
4502                    if (response.getType() != Iq.Type.RESULT) {
4503                        Log.d(
4504                                Config.LOGTAG,
4505                                account.getJid().asBareJid() + ": no vCard set. nothing to do");
4506                        return;
4507                    }
4508                    final Element vcard = response.findChild("vCard", "vcard-temp");
4509                    if (vcard == null) {
4510                        Log.d(
4511                                Config.LOGTAG,
4512                                account.getJid().asBareJid() + ": no vCard set. nothing to do");
4513                        return;
4514                    }
4515                    Element photo = vcard.findChild("PHOTO");
4516                    if (photo == null) {
4517                        photo = vcard.addChild("PHOTO");
4518                    }
4519                    photo.clearChildren();
4520                    final Iq publication = new Iq(Iq.Type.SET);
4521                    publication.setTo(account.getJid().asBareJid());
4522                    publication.addChild(vcard);
4523                    sendIqPacket(
4524                            account,
4525                            publication,
4526                            (publicationResponse) -> {
4527                                if (publicationResponse.getType() == Iq.Type.RESULT) {
4528                                    Log.d(
4529                                            Config.LOGTAG,
4530                                            account.getJid().asBareJid()
4531                                                    + ": successfully deleted vcard avatar");
4532                                    runnable.run();
4533                                } else {
4534                                    Log.d(
4535                                            Config.LOGTAG,
4536                                            "failed to publish vcard "
4537                                                    + publicationResponse.getErrorCondition());
4538                                }
4539                            });
4540                });
4541    }
4542
4543    private boolean hasEnabledAccounts() {
4544        if (this.accounts == null) {
4545            return false;
4546        }
4547        for (final Account account : this.accounts) {
4548            if (account.isConnectionEnabled()) {
4549                return true;
4550            }
4551        }
4552        return false;
4553    }
4554
4555    public void getAttachments(
4556            final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) {
4557        getAttachments(
4558                conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded);
4559    }
4560
4561    public void getAttachments(
4562            final Account account,
4563            final Jid jid,
4564            final int limit,
4565            final OnMediaLoaded onMediaLoaded) {
4566        getAttachments(account.getUuid(), jid.asBareJid(), limit, onMediaLoaded);
4567    }
4568
4569    public void getAttachments(
4570            final String account,
4571            final Jid jid,
4572            final int limit,
4573            final OnMediaLoaded onMediaLoaded) {
4574        new Thread(
4575                        () ->
4576                                onMediaLoaded.onMediaLoaded(
4577                                        fileBackend.convertToAttachments(
4578                                                databaseBackend.getRelativeFilePaths(
4579                                                        account, jid, limit))))
4580                .start();
4581    }
4582
4583    public void persistSelfNick(final MucOptions.User self, final boolean modified) {
4584        final Conversation conversation = self.getConversation();
4585        final Account account = conversation.getAccount();
4586        final Jid full = self.getFullJid();
4587        if (!full.equals(conversation.getJid())) {
4588            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persisting full jid " + full);
4589            conversation.setContactJid(full);
4590            databaseBackend.updateConversation(conversation);
4591        }
4592
4593        final String nick = self.getNick();
4594        final Bookmark bookmark = conversation.getBookmark();
4595        if (bookmark == null || !modified) {
4596            return;
4597        }
4598        final String defaultNick = MucOptions.defaultNick(account);
4599        if (nick.equals(defaultNick) || nick.equals(bookmark.getNick())) {
4600            return;
4601        }
4602        Log.d(
4603                Config.LOGTAG,
4604                account.getJid().asBareJid()
4605                        + ": persist nick '"
4606                        + full.getResource()
4607                        + "' into bookmark for "
4608                        + conversation.getJid().asBareJid());
4609        bookmark.setNick(nick);
4610        createBookmark(bookmark.getAccount(), bookmark);
4611    }
4612
4613    public void presenceToMuc(final Conversation conversation) {
4614        final MucOptions options = conversation.getMucOptions();
4615        if (options.online()) {
4616            Account account = conversation.getAccount();
4617            final Jid joinJid = options.getSelf().getFullJid();
4618            final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), options.getSelf().getNick());
4619            packet.setTo(joinJid);
4620            sendPresencePacket(account, packet);
4621        }
4622    }
4623
4624    public boolean renameInMuc(
4625            final Conversation conversation,
4626            final String nick,
4627            final UiCallback<Conversation> callback) {
4628        final Account account = conversation.getAccount();
4629        final Bookmark bookmark = conversation.getBookmark();
4630        final MucOptions options = conversation.getMucOptions();
4631        final Jid joinJid = options.createJoinJid(nick);
4632        if (joinJid == null) {
4633            return false;
4634        }
4635        if (options.online()) {
4636            maybeRegisterWithMuc(conversation, nick);
4637            options.setOnRenameListener(
4638                    new OnRenameListener() {
4639
4640                        @Override
4641                        public void onSuccess() {
4642                            final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick);
4643                            packet.setTo(joinJid);
4644                            sendPresencePacket(account, packet);
4645                            callback.success(conversation);
4646                        }
4647
4648                        @Override
4649                        public void onFailure() {
4650                            callback.error(R.string.nick_in_use, conversation);
4651                        }
4652                    });
4653
4654            final var packet =
4655                    mPresenceGenerator.selfPresence(
4656                            account, Presence.Status.ONLINE, options.nonanonymous(), nick);
4657            packet.setTo(joinJid);
4658            sendPresencePacket(account, packet);
4659            if (nick.equals(MucOptions.defaultNick(account))
4660                    && bookmark != null
4661                    && bookmark.getNick() != null) {
4662                Log.d(
4663                        Config.LOGTAG,
4664                        account.getJid().asBareJid()
4665                                + ": removing nick from bookmark for "
4666                                + bookmark.getJid());
4667                bookmark.setNick(null);
4668                createBookmark(account, bookmark);
4669            }
4670        } else {
4671            conversation.setContactJid(joinJid);
4672            databaseBackend.updateConversation(conversation);
4673            if (account.getStatus() == Account.State.ONLINE) {
4674                if (bookmark != null) {
4675                    bookmark.setNick(nick);
4676                    createBookmark(account, bookmark);
4677                }
4678                joinMuc(conversation);
4679            }
4680        }
4681        return true;
4682    }
4683
4684    public void checkMucRequiresRename() {
4685        synchronized (this.conversations) {
4686            for (final Conversation conversation : this.conversations) {
4687                if (conversation.getMode() == Conversational.MODE_MULTI) {
4688                    checkMucRequiresRename(conversation);
4689                }
4690            }
4691        }
4692    }
4693
4694    private void checkMucRequiresRename(final Conversation conversation) {
4695        final var options = conversation.getMucOptions();
4696        if (!options.online()) {
4697            return;
4698        }
4699        final var account = conversation.getAccount();
4700        final String current = options.getActualNick();
4701        final String proposed = options.getProposedNickPure();
4702        if (current == null || current.equals(proposed)) {
4703            return;
4704        }
4705        final Jid joinJid = options.createJoinJid(proposed);
4706        Log.d(
4707                Config.LOGTAG,
4708                String.format(
4709                        "%s: muc rename required %s (was: %s)",
4710                        account.getJid().asBareJid(), joinJid, current));
4711        final var packet =
4712                mPresenceGenerator.selfPresence(
4713                        account, Presence.Status.ONLINE, options.nonanonymous(), proposed);
4714        packet.setTo(joinJid);
4715        sendPresencePacket(account, packet);
4716    }
4717
4718    public void leaveMuc(Conversation conversation) {
4719        leaveMuc(conversation, false);
4720    }
4721
4722    private void leaveMuc(Conversation conversation, boolean now) {
4723        final Account account = conversation.getAccount();
4724        synchronized (account.pendingConferenceJoins) {
4725            account.pendingConferenceJoins.remove(conversation);
4726        }
4727        synchronized (account.pendingConferenceLeaves) {
4728            account.pendingConferenceLeaves.remove(conversation);
4729        }
4730        if (account.getStatus() == Account.State.ONLINE || now) {
4731            sendPresencePacket(
4732                    conversation.getAccount(),
4733                    mPresenceGenerator.leave(conversation.getMucOptions()));
4734            conversation.getMucOptions().setOffline();
4735            Bookmark bookmark = conversation.getBookmark();
4736            if (bookmark != null) {
4737                bookmark.setConversation(null);
4738            }
4739            Log.d(
4740                    Config.LOGTAG,
4741                    conversation.getAccount().getJid().asBareJid()
4742                            + ": leaving muc "
4743                            + conversation.getJid());
4744        } else {
4745            synchronized (account.pendingConferenceLeaves) {
4746                account.pendingConferenceLeaves.add(conversation);
4747            }
4748        }
4749    }
4750
4751    public String findConferenceServer(final Account account) {
4752        String server;
4753        if (account.getXmppConnection() != null) {
4754            server = account.getXmppConnection().getMucServer();
4755            if (server != null) {
4756                return server;
4757            }
4758        }
4759        for (Account other : getAccounts()) {
4760            if (other != account && other.getXmppConnection() != null) {
4761                server = other.getXmppConnection().getMucServer();
4762                if (server != null) {
4763                    return server;
4764                }
4765            }
4766        }
4767        return null;
4768    }
4769
4770    public void createPublicChannel(
4771            final Account account,
4772            final String name,
4773            final Jid address,
4774            final UiCallback<Conversation> callback) {
4775        joinMuc(
4776                findOrCreateConversation(account, address, true, false, true),
4777                conversation -> {
4778                    final Bundle configuration = IqGenerator.defaultChannelConfiguration();
4779                    if (!TextUtils.isEmpty(name)) {
4780                        configuration.putString("muc#roomconfig_roomname", name);
4781                    }
4782                    pushConferenceConfiguration(
4783                            conversation,
4784                            configuration,
4785                            new OnConfigurationPushed() {
4786                                @Override
4787                                public void onPushSucceeded() {
4788                                    saveConversationAsBookmark(conversation, name);
4789                                    callback.success(conversation);
4790                                }
4791
4792                                @Override
4793                                public void onPushFailed() {
4794                                    if (conversation
4795                                            .getMucOptions()
4796                                            .getSelf()
4797                                            .getAffiliation()
4798                                            .ranks(MucOptions.Affiliation.OWNER)) {
4799                                        callback.error(
4800                                                R.string.unable_to_set_channel_configuration,
4801                                                conversation);
4802                                    } else {
4803                                        callback.error(
4804                                                R.string.joined_an_existing_channel, conversation);
4805                                    }
4806                                }
4807                            });
4808                });
4809    }
4810
4811    public boolean createAdhocConference(
4812            final Account account,
4813            final String name,
4814            final Iterable<Jid> jids,
4815            final UiCallback<Conversation> callback) {
4816        Log.d(
4817                Config.LOGTAG,
4818                account.getJid().asBareJid().toString()
4819                        + ": creating adhoc conference with "
4820                        + jids.toString());
4821        if (account.getStatus() == Account.State.ONLINE) {
4822            try {
4823                String server = findConferenceServer(account);
4824                if (server == null) {
4825                    if (callback != null) {
4826                        callback.error(R.string.no_conference_server_found, null);
4827                    }
4828                    return false;
4829                }
4830                final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null);
4831                final Conversation conversation =
4832                        findOrCreateConversation(account, jid, true, false, true);
4833                joinMuc(
4834                        conversation,
4835                        new OnConferenceJoined() {
4836                            @Override
4837                            public void onConferenceJoined(final Conversation conversation) {
4838                                final Bundle configuration =
4839                                        IqGenerator.defaultGroupChatConfiguration();
4840                                if (!TextUtils.isEmpty(name)) {
4841                                    configuration.putString("muc#roomconfig_roomname", name);
4842                                }
4843                                pushConferenceConfiguration(
4844                                        conversation,
4845                                        configuration,
4846                                        new OnConfigurationPushed() {
4847                                            @Override
4848                                            public void onPushSucceeded() {
4849                                                for (Jid invite : jids) {
4850                                                    invite(conversation, invite);
4851                                                }
4852                                                for (String resource :
4853                                                        account.getSelfContact()
4854                                                                .getPresences()
4855                                                                .toResourceArray()) {
4856                                                    Jid other =
4857                                                            account.getJid().withResource(resource);
4858                                                    Log.d(
4859                                                            Config.LOGTAG,
4860                                                            account.getJid().asBareJid()
4861                                                                    + ": sending direct invite to "
4862                                                                    + other);
4863                                                    directInvite(conversation, other);
4864                                                }
4865                                                saveConversationAsBookmark(conversation, name);
4866                                                if (callback != null) {
4867                                                    callback.success(conversation);
4868                                                }
4869                                            }
4870
4871                                            @Override
4872                                            public void onPushFailed() {
4873                                                archiveConversation(conversation);
4874                                                if (callback != null) {
4875                                                    callback.error(
4876                                                            R.string.conference_creation_failed,
4877                                                            conversation);
4878                                                }
4879                                            }
4880                                        });
4881                            }
4882                        });
4883                return true;
4884            } catch (IllegalArgumentException e) {
4885                if (callback != null) {
4886                    callback.error(R.string.conference_creation_failed, null);
4887                }
4888                return false;
4889            }
4890        } else {
4891            if (callback != null) {
4892                callback.error(R.string.not_connected_try_again, null);
4893            }
4894            return false;
4895        }
4896    }
4897
4898    public void checkIfMuc(final Account account, final Jid jid, Consumer<Boolean> cb) {
4899        if (jid.isDomainJid()) {
4900            // Spec basically says MUC needs to have a node
4901            // And also specifies that MUC and MUC service should have the same identity...
4902            cb.accept(false);
4903            return;
4904        }
4905
4906        final var request = mIqGenerator.queryDiscoInfo(jid.asBareJid());
4907        sendIqPacket(account, request, (reply) -> {
4908            final var result = new ServiceDiscoveryResult(reply);
4909            cb.accept(
4910                result.getFeatures().contains("http://jabber.org/protocol/muc") &&
4911                result.hasIdentity("conference", null)
4912            );
4913        });
4914    }
4915
4916    public void fetchConferenceConfiguration(final Conversation conversation) {
4917        fetchConferenceConfiguration(conversation, null);
4918    }
4919
4920    public void fetchConferenceConfiguration(
4921            final Conversation conversation, final OnConferenceConfigurationFetched callback) {
4922        final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid());
4923        final var account = conversation.getAccount();
4924        sendIqPacket(
4925                account,
4926                request,
4927                response -> {
4928                    if (response.getType() == Iq.Type.RESULT) {
4929                        final MucOptions mucOptions = conversation.getMucOptions();
4930                        final Bookmark bookmark = conversation.getBookmark();
4931                        final boolean sameBefore =
4932                                StringUtils.equals(
4933                                        bookmark == null ? null : bookmark.getBookmarkName(),
4934                                        mucOptions.getName());
4935
4936                        if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(response))) {
4937                            Log.d(
4938                                    Config.LOGTAG,
4939                                    account.getJid().asBareJid()
4940                                            + ": muc configuration changed for "
4941                                            + conversation.getJid().asBareJid());
4942                            updateConversation(conversation);
4943                        }
4944
4945                        if (bookmark != null
4946                                && (sameBefore || bookmark.getBookmarkName() == null)) {
4947                            if (bookmark.setBookmarkName(
4948                                    StringUtils.nullOnEmpty(mucOptions.getName()))) {
4949                                createBookmark(account, bookmark);
4950                            }
4951                        }
4952
4953                        if (callback != null) {
4954                            callback.onConferenceConfigurationFetched(conversation);
4955                        }
4956
4957                        updateConversationUi();
4958                    } else if (response.getType() == Iq.Type.TIMEOUT) {
4959                        Log.d(
4960                                Config.LOGTAG,
4961                                account.getJid().asBareJid()
4962                                        + ": received timeout waiting for conference configuration"
4963                                        + " fetch");
4964                    } else {
4965                        if (callback != null) {
4966                            callback.onFetchFailed(conversation, response.getErrorCondition());
4967                        }
4968                    }
4969                });
4970    }
4971
4972    public void pushNodeConfiguration(
4973            Account account,
4974            final String node,
4975            final Bundle options,
4976            final OnConfigurationPushed callback) {
4977        pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback);
4978    }
4979
4980    public void pushNodeConfiguration(
4981            Account account,
4982            final Jid jid,
4983            final String node,
4984            final Bundle options,
4985            final OnConfigurationPushed callback) {
4986        Log.d(Config.LOGTAG, "pushing node configuration");
4987        sendIqPacket(
4988                account,
4989                mIqGenerator.requestPubsubConfiguration(jid, node),
4990                responseToRequest -> {
4991                    if (responseToRequest.getType() == Iq.Type.RESULT) {
4992                        Element pubsub =
4993                                responseToRequest.findChild(
4994                                        "pubsub", "http://jabber.org/protocol/pubsub#owner");
4995                        Element configuration =
4996                                pubsub == null ? null : pubsub.findChild("configure");
4997                        Element x =
4998                                configuration == null
4999                                        ? null
5000                                        : configuration.findChild("x", Namespace.DATA);
5001                        if (x != null) {
5002                            final Data data = Data.parse(x);
5003                            data.submit(options);
5004                            sendIqPacket(
5005                                    account,
5006                                    mIqGenerator.publishPubsubConfiguration(jid, node, data),
5007                                    responseToPublish -> {
5008                                        if (responseToPublish.getType() == Iq.Type.RESULT
5009                                                && callback != null) {
5010                                            Log.d(
5011                                                    Config.LOGTAG,
5012                                                    account.getJid().asBareJid()
5013                                                            + ": successfully changed node"
5014                                                            + " configuration for node "
5015                                                            + node);
5016                                            callback.onPushSucceeded();
5017                                        } else if (responseToPublish.getType() == Iq.Type.ERROR
5018                                                && callback != null) {
5019                                            callback.onPushFailed();
5020                                        }
5021                                    });
5022                        } else if (callback != null) {
5023                            callback.onPushFailed();
5024                        }
5025                    } else if (responseToRequest.getType() == Iq.Type.ERROR && callback != null) {
5026                        callback.onPushFailed();
5027                    }
5028                });
5029    }
5030
5031    public void pushConferenceConfiguration(
5032            final Conversation conversation,
5033            final Bundle options,
5034            final OnConfigurationPushed callback) {
5035        if (options.getString("muc#roomconfig_whois", "moderators").equals("anyone")) {
5036            conversation.setAttribute("accept_non_anonymous", true);
5037            updateConversation(conversation);
5038        }
5039        if (options.containsKey("muc#roomconfig_moderatedroom")) {
5040            final boolean moderated = "1".equals(options.getString("muc#roomconfig_moderatedroom"));
5041            options.putString("members_by_default", moderated ? "0" : "1");
5042        }
5043        if (options.containsKey("muc#roomconfig_allowpm")) {
5044            // ejabberd :-/
5045            final boolean allow = "anyone".equals(options.getString("muc#roomconfig_allowpm"));
5046            options.putString("allow_private_messages", allow ? "1" : "0");
5047            options.putString("allow_private_messages_from_visitors", allow ? "anyone" : "nobody");
5048        }
5049        final var account = conversation.getAccount();
5050        final Iq request = new Iq(Iq.Type.GET);
5051        request.setTo(conversation.getJid().asBareJid());
5052        request.query("http://jabber.org/protocol/muc#owner");
5053        sendIqPacket(
5054                account,
5055                request,
5056                response -> {
5057                    if (response.getType() == Iq.Type.RESULT) {
5058                        final Data data =
5059                                Data.parse(response.query().findChild("x", Namespace.DATA));
5060                        data.submit(options);
5061                        final Iq set = new Iq(Iq.Type.SET);
5062                        set.setTo(conversation.getJid().asBareJid());
5063                        set.query("http://jabber.org/protocol/muc#owner").addChild(data);
5064                        sendIqPacket(
5065                                account,
5066                                set,
5067                                packet -> {
5068                                    if (callback != null) {
5069                                        if (packet.getType() == Iq.Type.RESULT) {
5070                                            callback.onPushSucceeded();
5071                                        } else {
5072                                            Log.d(Config.LOGTAG, "failed: " + packet.toString());
5073                                            callback.onPushFailed();
5074                                        }
5075                                    }
5076                                });
5077                    } else {
5078                        if (callback != null) {
5079                            callback.onPushFailed();
5080                        }
5081                    }
5082                });
5083    }
5084
5085    public void pushSubjectToConference(final Conversation conference, final String subject) {
5086        final var packet =
5087                this.getMessageGenerator()
5088                        .conferenceSubject(conference, StringUtils.nullOnEmpty(subject));
5089        this.sendMessagePacket(conference.getAccount(), packet);
5090    }
5091
5092    public void requestVoice(final Account account, final Jid jid) {
5093        final var packet = this.getMessageGenerator().requestVoice(jid);
5094        this.sendMessagePacket(account, packet);
5095    }
5096
5097    public void changeAffiliationInConference(
5098            final Conversation conference,
5099            Jid user,
5100            final MucOptions.Affiliation affiliation,
5101            final OnAffiliationChanged callback) {
5102        final Jid jid = user.asBareJid();
5103        final Iq request =
5104                this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
5105        sendIqPacket(
5106                conference.getAccount(),
5107                request,
5108                (response) -> {
5109                    if (response.getType() == Iq.Type.RESULT) {
5110                        final var mucOptions = conference.getMucOptions();
5111                        mucOptions.changeAffiliation(jid, affiliation);
5112                        getAvatarService().clear(mucOptions);
5113                        if (callback != null) {
5114                            callback.onAffiliationChangedSuccessful(jid);
5115                        } else {
5116                            Log.d(
5117                                    Config.LOGTAG,
5118                                    "changed affiliation of " + user + " to " + affiliation);
5119                        }
5120                    } else if (callback != null) {
5121                        callback.onAffiliationChangeFailed(
5122                                jid, R.string.could_not_change_affiliation);
5123                    } else {
5124                        Log.d(Config.LOGTAG, "unable to change affiliation");
5125                    }
5126                });
5127    }
5128
5129    public void changeRoleInConference(
5130            final Conversation conference, final String nick, MucOptions.Role role) {
5131        final var account = conference.getAccount();
5132        final Iq request = this.mIqGenerator.changeRole(conference, nick, role.toString());
5133        sendIqPacket(
5134                account,
5135                request,
5136                (packet) -> {
5137                    if (packet.getType() != Iq.Type.RESULT) {
5138                        Log.d(
5139                                Config.LOGTAG,
5140                                account.getJid().asBareJid() + " unable to change role of " + nick);
5141                    }
5142                });
5143    }
5144
5145    public void moderateMessage(final Account account, final Message m, final String reason) {
5146        final var request = this.mIqGenerator.moderateMessage(account, m, reason);
5147        sendIqPacket(account, request, (packet) -> {
5148            if (packet.getType() != Iq.Type.RESULT) {
5149                showErrorToastInUi(R.string.unable_to_moderate);
5150                Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to moderate: " + packet);
5151            }
5152        });
5153    }
5154
5155    public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) {
5156        final Iq request = new Iq(Iq.Type.SET);
5157        request.setTo(conversation.getJid().asBareJid());
5158        request.query("http://jabber.org/protocol/muc#owner").addChild("destroy");
5159        sendIqPacket(
5160                conversation.getAccount(),
5161                request,
5162                response -> {
5163                    if (response.getType() == Iq.Type.RESULT) {
5164                        if (callback != null) {
5165                            callback.onRoomDestroySucceeded();
5166                        }
5167                    } else if (response.getType() == Iq.Type.ERROR) {
5168                        if (callback != null) {
5169                            callback.onRoomDestroyFailed();
5170                        }
5171                    }
5172                });
5173    }
5174
5175    private void disconnect(final Account account, boolean force) {
5176        final XmppConnection connection = account.getXmppConnection();
5177        if (connection == null) {
5178            return;
5179        }
5180        if (!force) {
5181            final List<Conversation> conversations = getConversations();
5182            for (Conversation conversation : conversations) {
5183                if (conversation.getAccount() == account) {
5184                    if (conversation.getMode() == Conversation.MODE_MULTI) {
5185                        leaveMuc(conversation, true);
5186                    }
5187                }
5188            }
5189            sendOfflinePresence(account);
5190        }
5191        connection.disconnect(force);
5192    }
5193
5194    @Override
5195    public IBinder onBind(Intent intent) {
5196        return mBinder;
5197    }
5198
5199    public void deleteMessage(Message message) {
5200        mScheduledMessages.remove(message.getUuid());
5201        databaseBackend.deleteMessage(message.getUuid());
5202        ((Conversation) message.getConversation()).remove(message);
5203        updateConversationUi();
5204    }
5205
5206    public void updateMessage(Message message) {
5207        updateMessage(message, true);
5208    }
5209
5210    public void updateMessage(Message message, boolean includeBody) {
5211        databaseBackend.updateMessage(message, includeBody);
5212        updateConversationUi();
5213    }
5214
5215    public void createMessageAsync(final Message message) {
5216        mDatabaseWriterExecutor.execute(() -> databaseBackend.createMessage(message));
5217    }
5218
5219    public void updateMessage(Message message, String uuid) {
5220        if (!databaseBackend.updateMessage(message, uuid)) {
5221            Log.e(Config.LOGTAG, "error updated message in DB after edit");
5222        }
5223        updateConversationUi();
5224    }
5225
5226    public void syncDirtyContacts(Account account) {
5227        for (Contact contact : account.getRoster().getContacts()) {
5228            if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
5229                pushContactToServer(contact);
5230            }
5231            if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
5232                deleteContactOnServer(contact);
5233            }
5234        }
5235    }
5236
5237    protected void unregisterPhoneAccounts(final Account account) {
5238        for (final Contact contact : account.getRoster().getContacts()) {
5239            if (!contact.showInRoster()) {
5240                contact.unregisterAsPhoneAccount(this);
5241            }
5242        }
5243    }
5244
5245    public void createContact(final Contact contact, final boolean autoGrant) {
5246        createContact(contact, autoGrant, null);
5247    }
5248
5249    public void createContact(
5250            final Contact contact, final boolean autoGrant, final String preAuth) {
5251        if (autoGrant) {
5252            contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
5253            contact.setOption(Contact.Options.ASKING);
5254        }
5255        pushContactToServer(contact, preAuth);
5256    }
5257
5258    public void pushContactToServer(final Contact contact) {
5259        pushContactToServer(contact, null);
5260    }
5261
5262    private void pushContactToServer(final Contact contact, final String preAuth) {
5263        contact.resetOption(Contact.Options.DIRTY_DELETE);
5264        contact.setOption(Contact.Options.DIRTY_PUSH);
5265        final Account account = contact.getAccount();
5266        if (account.getStatus() == Account.State.ONLINE) {
5267            final boolean ask = contact.getOption(Contact.Options.ASKING);
5268            final boolean sendUpdates =
5269                    contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
5270                            && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
5271            final Iq iq = new Iq(Iq.Type.SET);
5272            iq.query(Namespace.ROSTER).addChild(contact.asElement());
5273            account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
5274            if (sendUpdates) {
5275                sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
5276            }
5277            if (ask) {
5278                sendPresencePacket(
5279                        account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth));
5280            }
5281        } else {
5282            syncRoster(contact.getAccount());
5283        }
5284    }
5285
5286    public void publishMucAvatar(
5287            final Conversation conversation, final Uri image, final OnAvatarPublication callback) {
5288        new Thread(
5289                        () -> {
5290                            final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
5291                            final int size = Config.AVATAR_SIZE;
5292                            final Avatar avatar =
5293                                    getFileBackend().getPepAvatar(image, size, format);
5294                            if (avatar != null) {
5295                                if (!getFileBackend().save(avatar)) {
5296                                    callback.onAvatarPublicationFailed(
5297                                            R.string.error_saving_avatar);
5298                                    return;
5299                                }
5300                                avatar.owner = conversation.getJid().asBareJid();
5301                                publishMucAvatar(conversation, avatar, callback);
5302                            } else {
5303                                callback.onAvatarPublicationFailed(
5304                                        R.string.error_publish_avatar_converting);
5305                            }
5306                        })
5307                .start();
5308    }
5309
5310    public void publishAvatarAsync(
5311            final Account account,
5312            final Uri image,
5313            final boolean open,
5314            final OnAvatarPublication callback) {
5315        new Thread(() -> publishAvatar(account, image, open, callback)).start();
5316    }
5317
5318    private void publishAvatar(
5319            final Account account,
5320            final Uri image,
5321            final boolean open,
5322            final OnAvatarPublication callback) {
5323        final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
5324        final int size = Config.AVATAR_SIZE;
5325        final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
5326        if (avatar != null) {
5327            if (!getFileBackend().save(avatar)) {
5328                Log.d(Config.LOGTAG, "unable to save vcard");
5329                callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
5330                return;
5331            }
5332            publishAvatar(account, avatar, open, callback);
5333        } else {
5334            callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
5335        }
5336    }
5337
5338    private void publishMucAvatar(
5339            Conversation conversation, Avatar avatar, OnAvatarPublication callback) {
5340        final var account = conversation.getAccount();
5341        final Iq retrieve = mIqGenerator.retrieveVcardAvatar(avatar);
5342        sendIqPacket(
5343                account,
5344                retrieve,
5345                (response) -> {
5346                    boolean itemNotFound =
5347                            response.getType() == Iq.Type.ERROR
5348                                    && response.hasChild("error")
5349                                    && response.findChild("error").hasChild("item-not-found");
5350                    if (response.getType() == Iq.Type.RESULT || itemNotFound) {
5351                        Element vcard = response.findChild("vCard", "vcard-temp");
5352                        if (vcard == null) {
5353                            vcard = new Element("vCard", "vcard-temp");
5354                        }
5355                        Element photo = vcard.findChild("PHOTO");
5356                        if (photo == null) {
5357                            photo = vcard.addChild("PHOTO");
5358                        }
5359                        photo.clearChildren();
5360                        photo.addChild("TYPE").setContent(avatar.type);
5361                        photo.addChild("BINVAL").setContent(avatar.image);
5362                        final Iq publication = new Iq(Iq.Type.SET);
5363                        publication.setTo(conversation.getJid().asBareJid());
5364                        publication.addChild(vcard);
5365                        sendIqPacket(
5366                                account,
5367                                publication,
5368                                (publicationResponse) -> {
5369                                    if (publicationResponse.getType() == Iq.Type.RESULT) {
5370                                        callback.onAvatarPublicationSucceeded();
5371                                    } else {
5372                                        Log.d(
5373                                                Config.LOGTAG,
5374                                                "failed to publish vcard "
5375                                                        + publicationResponse.getErrorCondition());
5376                                        callback.onAvatarPublicationFailed(
5377                                                R.string.error_publish_avatar_server_reject);
5378                                    }
5379                                });
5380                    } else {
5381                        Log.d(Config.LOGTAG, "failed to request vcard " + response);
5382                        callback.onAvatarPublicationFailed(
5383                                R.string.error_publish_avatar_no_server_support);
5384                    }
5385                });
5386    }
5387
5388    public void publishAvatar(
5389            final Account account,
5390            final Avatar avatar,
5391            final boolean open,
5392            final OnAvatarPublication callback) {
5393        final Bundle options;
5394        if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
5395            options = open ? PublishOptions.openAccess() : PublishOptions.presenceAccess();
5396        } else {
5397            options = null;
5398        }
5399        publishAvatar(account, avatar, options, true, callback);
5400    }
5401
5402    public void publishAvatar(
5403            Account account,
5404            final Avatar avatar,
5405            final Bundle options,
5406            final boolean retry,
5407            final OnAvatarPublication callback) {
5408        Log.d(
5409                Config.LOGTAG,
5410                account.getJid().asBareJid() + ": publishing avatar. options=" + options);
5411        final Iq packet = this.mIqGenerator.publishAvatar(avatar, options);
5412        this.sendIqPacket(
5413                account,
5414                packet,
5415                result -> {
5416                    if (result.getType() == Iq.Type.RESULT) {
5417                        publishAvatarMetadata(account, avatar, options, true, callback);
5418                    } else if (retry && PublishOptions.preconditionNotMet(result)) {
5419                        pushNodeConfiguration(
5420                                account,
5421                                Namespace.AVATAR_DATA,
5422                                options,
5423                                new OnConfigurationPushed() {
5424                                    @Override
5425                                    public void onPushSucceeded() {
5426                                        Log.d(
5427                                                Config.LOGTAG,
5428                                                account.getJid().asBareJid()
5429                                                        + ": changed node configuration for avatar"
5430                                                        + " node");
5431                                        publishAvatar(account, avatar, options, false, callback);
5432                                    }
5433
5434                                    @Override
5435                                    public void onPushFailed() {
5436                                        Log.d(
5437                                                Config.LOGTAG,
5438                                                account.getJid().asBareJid()
5439                                                        + ": unable to change node configuration"
5440                                                        + " for avatar node");
5441                                        publishAvatar(account, avatar, null, false, callback);
5442                                    }
5443                                });
5444                    } else {
5445                        Element error = result.findChild("error");
5446                        Log.d(
5447                                Config.LOGTAG,
5448                                account.getJid().asBareJid()
5449                                        + ": server rejected avatar "
5450                                        + (avatar.size / 1024)
5451                                        + "KiB "
5452                                        + (error != null ? error.toString() : ""));
5453                        if (callback != null) {
5454                            callback.onAvatarPublicationFailed(
5455                                    R.string.error_publish_avatar_server_reject);
5456                        }
5457                    }
5458                });
5459    }
5460
5461    public void publishAvatarMetadata(
5462            Account account,
5463            final Avatar avatar,
5464            final Bundle options,
5465            final boolean retry,
5466            final OnAvatarPublication callback) {
5467        final Iq packet =
5468                XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options);
5469        sendIqPacket(
5470                account,
5471                packet,
5472                result -> {
5473                    if (result.getType() == Iq.Type.RESULT) {
5474                        if (account.setAvatar(avatar.getFilename())) {
5475                            getAvatarService().clear(account);
5476                            databaseBackend.updateAccount(account);
5477                            notifyAccountAvatarHasChanged(account);
5478                        }
5479                        Log.d(
5480                                Config.LOGTAG,
5481                                account.getJid().asBareJid()
5482                                        + ": published avatar "
5483                                        + (avatar.size / 1024)
5484                                        + "KiB");
5485                        if (callback != null) {
5486                            callback.onAvatarPublicationSucceeded();
5487                        }
5488                    } else if (retry && PublishOptions.preconditionNotMet(result)) {
5489                        pushNodeConfiguration(
5490                                account,
5491                                Namespace.AVATAR_METADATA,
5492                                options,
5493                                new OnConfigurationPushed() {
5494                                    @Override
5495                                    public void onPushSucceeded() {
5496                                        Log.d(
5497                                                Config.LOGTAG,
5498                                                account.getJid().asBareJid()
5499                                                        + ": changed node configuration for avatar"
5500                                                        + " meta data node");
5501                                        publishAvatarMetadata(
5502                                                account, avatar, options, false, callback);
5503                                    }
5504
5505                                    @Override
5506                                    public void onPushFailed() {
5507                                        Log.d(
5508                                                Config.LOGTAG,
5509                                                account.getJid().asBareJid()
5510                                                        + ": unable to change node configuration"
5511                                                        + " for avatar meta data node");
5512                                        publishAvatarMetadata(
5513                                                account, avatar, null, false, callback);
5514                                    }
5515                                });
5516                    } else {
5517                        if (callback != null) {
5518                            callback.onAvatarPublicationFailed(
5519                                    R.string.error_publish_avatar_server_reject);
5520                        }
5521                    }
5522                });
5523    }
5524
5525    public void republishAvatarIfNeeded(final Account account) {
5526        if (account.getAxolotlService().isPepBroken()) {
5527            Log.d(
5528                    Config.LOGTAG,
5529                    account.getJid().asBareJid()
5530                            + ": skipping republication of avatar because pep is broken");
5531            return;
5532        }
5533        final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
5534        this.sendIqPacket(
5535                account,
5536                packet,
5537                new Consumer<Iq>() {
5538
5539                    private Avatar parseAvatar(Iq packet) {
5540                        Element pubsub =
5541                                packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
5542                        if (pubsub != null) {
5543                            Element items = pubsub.findChild("items");
5544                            if (items != null) {
5545                                return Avatar.parseMetadata(items);
5546                            }
5547                        }
5548                        return null;
5549                    }
5550
5551                    private boolean errorIsItemNotFound(Iq packet) {
5552                        Element error = packet.findChild("error");
5553                        return packet.getType() == Iq.Type.ERROR
5554                                && error != null
5555                                && error.hasChild("item-not-found");
5556                    }
5557
5558                    @Override
5559                    public void accept(final Iq packet) {
5560                        if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) {
5561                            final Avatar serverAvatar = parseAvatar(packet);
5562                            if (serverAvatar == null && account.getAvatar() != null) {
5563                                final Avatar avatar =
5564                                        fileBackend.getStoredPepAvatar(account.getAvatar());
5565                                if (avatar != null) {
5566                                    Log.d(
5567                                            Config.LOGTAG,
5568                                            account.getJid().asBareJid()
5569                                                    + ": avatar on server was null. republishing");
5570                                    // publishing as 'open' - old server (that requires
5571                                    // republication) likely doesn't support access models anyway
5572                                    publishAvatar(
5573                                            account,
5574                                            fileBackend.getStoredPepAvatar(account.getAvatar()),
5575                                            true,
5576                                            null);
5577                                } else {
5578                                    Log.e(
5579                                            Config.LOGTAG,
5580                                            account.getJid().asBareJid()
5581                                                    + ": error rereading avatar");
5582                                }
5583                            }
5584                        }
5585                    }
5586                });
5587    }
5588
5589    public void cancelAvatarFetches(final Account account) {
5590        synchronized (mInProgressAvatarFetches) {
5591            for (final Iterator<String> iterator = mInProgressAvatarFetches.iterator();
5592                    iterator.hasNext(); ) {
5593                final String KEY = iterator.next();
5594                if (KEY.startsWith(account.getJid().asBareJid() + "_")) {
5595                    iterator.remove();
5596                }
5597            }
5598        }
5599    }
5600
5601    public void fetchAvatar(Account account, Avatar avatar) {
5602        fetchAvatar(account, avatar, null);
5603    }
5604
5605    public void fetchAvatar(
5606            Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5607        if (databaseBackend.isBlockedMedia(avatar.cid())) {
5608            if (callback != null) callback.error(0, null);
5609            return;
5610        }
5611
5612        final String KEY = generateFetchKey(account, avatar);
5613        synchronized (this.mInProgressAvatarFetches) {
5614            if (mInProgressAvatarFetches.add(KEY)) {
5615                switch (avatar.origin) {
5616                    case PEP:
5617                        this.mInProgressAvatarFetches.add(KEY);
5618                        fetchAvatarPep(account, avatar, callback);
5619                        break;
5620                    case VCARD:
5621                        this.mInProgressAvatarFetches.add(KEY);
5622                        fetchAvatarVcard(account, avatar, callback);
5623                        break;
5624                }
5625            } else if (avatar.origin == Avatar.Origin.PEP) {
5626                mOmittedPepAvatarFetches.add(KEY);
5627            } else {
5628                Log.d(
5629                        Config.LOGTAG,
5630                        account.getJid().asBareJid()
5631                                + ": already fetching "
5632                                + avatar.origin
5633                                + " avatar for "
5634                                + avatar.owner);
5635            }
5636        }
5637    }
5638
5639    private void fetchAvatarPep(
5640            final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5641        final Iq packet = this.mIqGenerator.retrievePepAvatar(avatar);
5642        sendIqPacket(
5643                account,
5644                packet,
5645                (result) -> {
5646                    synchronized (mInProgressAvatarFetches) {
5647                        mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
5648                    }
5649                    final String ERROR =
5650                            account.getJid().asBareJid()
5651                                    + ": fetching avatar for "
5652                                    + avatar.owner
5653                                    + " failed ";
5654                    if (result.getType() == Iq.Type.RESULT) {
5655                        avatar.image = IqParser.avatarData(result);
5656                        if (avatar.image != null) {
5657                            if (getFileBackend().save(avatar)) {
5658                                if (account.getJid().asBareJid().equals(avatar.owner)) {
5659                                    if (account.setAvatar(avatar.getFilename())) {
5660                                        databaseBackend.updateAccount(account);
5661                                    }
5662                                    getAvatarService().clear(account);
5663                                    updateConversationUi();
5664                                    updateAccountUi();
5665                                } else {
5666                                    final Contact contact =
5667                                            account.getRoster().getContact(avatar.owner);
5668                                    contact.setAvatar(avatar);
5669                                    syncRoster(account);
5670                                    getAvatarService().clear(contact);
5671                                    updateConversationUi();
5672                                    updateRosterUi(UpdateRosterReason.AVATAR);
5673                                }
5674                                if (callback != null) {
5675                                    callback.success(avatar);
5676                                }
5677                                Log.d(
5678                                        Config.LOGTAG,
5679                                        account.getJid().asBareJid()
5680                                                + ": successfully fetched pep avatar for "
5681                                                + avatar.owner);
5682                                return;
5683                            }
5684                        } else {
5685
5686                            Log.d(Config.LOGTAG, ERROR + "(parsing error)");
5687                        }
5688                    } else {
5689                        Element error = result.findChild("error");
5690                        if (error == null) {
5691                            Log.d(Config.LOGTAG, ERROR + "(server error)");
5692                        } else {
5693                            Log.d(Config.LOGTAG, ERROR + error.toString());
5694                        }
5695                    }
5696                    if (callback != null) {
5697                        callback.error(0, null);
5698                    }
5699                });
5700    }
5701
5702    private void fetchAvatarVcard(
5703            final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5704        final Iq packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
5705        this.sendIqPacket(
5706                account,
5707                packet,
5708                response -> {
5709                    final boolean previouslyOmittedPepFetch;
5710                    synchronized (mInProgressAvatarFetches) {
5711                        final String KEY = generateFetchKey(account, avatar);
5712                        mInProgressAvatarFetches.remove(KEY);
5713                        previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY);
5714                    }
5715                    if (response.getType() == Iq.Type.RESULT) {
5716                        Element vCard = response.findChild("vCard", "vcard-temp");
5717                        Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
5718                        String image = photo != null ? photo.findChildContent("BINVAL") : null;
5719                        if (image != null) {
5720                            avatar.image = image;
5721                            if (getFileBackend().save(avatar)) {
5722                                Log.d(
5723                                        Config.LOGTAG,
5724                                        account.getJid().asBareJid()
5725                                                + ": successfully fetched vCard avatar for "
5726                                                + avatar.owner
5727                                                + " omittedPep="
5728                                                + previouslyOmittedPepFetch);
5729                                if (avatar.owner.isBareJid()) {
5730                                    if (account.getJid().asBareJid().equals(avatar.owner)
5731                                            && account.getAvatar() == null) {
5732                                        Log.d(
5733                                                Config.LOGTAG,
5734                                                account.getJid().asBareJid()
5735                                                        + ": had no avatar. replacing with vcard");
5736                                        account.setAvatar(avatar.getFilename());
5737                                        databaseBackend.updateAccount(account);
5738                                        getAvatarService().clear(account);
5739                                        updateAccountUi();
5740                                    } else {
5741                                        final Contact contact =
5742                                                account.getRoster().getContact(avatar.owner);
5743                                        contact.setAvatar(avatar, previouslyOmittedPepFetch);
5744                                        syncRoster(account);
5745                                        getAvatarService().clear(contact);
5746                                        updateRosterUi(UpdateRosterReason.AVATAR);
5747                                    }
5748                                    updateConversationUi();
5749                                } else {
5750                                    Conversation conversation =
5751                                            find(account, avatar.owner.asBareJid());
5752                                    if (conversation != null
5753                                            && conversation.getMode() == Conversation.MODE_MULTI) {
5754                                        MucOptions.User user =
5755                                                conversation
5756                                                        .getMucOptions()
5757                                                        .findUserByFullJid(avatar.owner);
5758                                        if (user != null) {
5759                                            if (user.setAvatar(avatar)) {
5760                                                getAvatarService().clear(user);
5761                                                updateConversationUi();
5762                                                updateMucRosterUi();
5763                                            }
5764                                            if (user.getRealJid() != null) {
5765                                                Contact contact =
5766                                                        account.getRoster()
5767                                                                .getContact(user.getRealJid());
5768                                                contact.setAvatar(avatar);
5769                                                syncRoster(account);
5770                                                getAvatarService().clear(contact);
5771                                                updateRosterUi(UpdateRosterReason.AVATAR);
5772                                            }
5773                                        }
5774                                    }
5775                                }
5776                            }
5777                        }
5778                    }
5779                });
5780    }
5781
5782    public void checkForAvatar(final Account account, final UiCallback<Avatar> callback) {
5783        final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
5784        this.sendIqPacket(
5785                account,
5786                packet,
5787                response -> {
5788                    if (response.getType() == Iq.Type.RESULT) {
5789                        Element pubsub =
5790                                response.findChild("pubsub", "http://jabber.org/protocol/pubsub");
5791                        if (pubsub != null) {
5792                            Element items = pubsub.findChild("items");
5793                            if (items != null) {
5794                                Avatar avatar = Avatar.parseMetadata(items);
5795                                if (avatar != null) {
5796                                    avatar.owner = account.getJid().asBareJid();
5797                                    if (fileBackend.isAvatarCached(avatar)) {
5798                                        if (account.setAvatar(avatar.getFilename())) {
5799                                            databaseBackend.updateAccount(account);
5800                                        }
5801                                        getAvatarService().clear(account);
5802                                        callback.success(avatar);
5803                                    } else {
5804                                        fetchAvatarPep(account, avatar, callback);
5805                                    }
5806                                    return;
5807                                }
5808                            }
5809                        }
5810                    }
5811                    callback.error(0, null);
5812                });
5813    }
5814
5815    public void notifyAccountAvatarHasChanged(final Account account) {
5816        final XmppConnection connection = account.getXmppConnection();
5817        if (connection != null && connection.getFeatures().bookmarksConversion()) {
5818            Log.d(
5819                    Config.LOGTAG,
5820                    account.getJid().asBareJid()
5821                            + ": avatar changed. resending presence to online group chats");
5822            for (Conversation conversation : conversations) {
5823                if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) {
5824                    presenceToMuc(conversation);
5825                }
5826            }
5827        }
5828    }
5829
5830    public void fetchVcard4(Account account, final Contact contact, final Consumer<Element> callback) {
5831        final var packet = this.mIqGenerator.retrieveVcard4(contact.getJid());
5832        sendIqPacket(account, packet, (result) -> {
5833            if (result.getType() == Iq.Type.RESULT) {
5834                final Element item = IqParser.getItem(result);
5835                if (item != null) {
5836                    final Element vcard4 = item.findChild("vcard", Namespace.VCARD4);
5837                    if (vcard4 != null) {
5838                        if (callback != null) {
5839                            callback.accept(vcard4);
5840                        }
5841                        return;
5842                    }
5843                }
5844            } else {
5845                Element error = result.findChild("error");
5846                if (error == null) {
5847                    Log.d(Config.LOGTAG, "fetchVcard4 (server error)");
5848                } else {
5849                    Log.d(Config.LOGTAG, "fetchVcard4 " + error.toString());
5850                }
5851            }
5852            if (callback != null) {
5853                callback.accept(null);
5854            }
5855
5856        });
5857    }
5858
5859    public void deleteContactOnServer(Contact contact) {
5860        contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
5861        contact.resetOption(Contact.Options.DIRTY_PUSH);
5862        contact.setOption(Contact.Options.DIRTY_DELETE);
5863        Account account = contact.getAccount();
5864        if (account.getStatus() == Account.State.ONLINE) {
5865            final Iq iq = new Iq(Iq.Type.SET);
5866            Element item = iq.query(Namespace.ROSTER).addChild("item");
5867            item.setAttribute("jid", contact.getJid());
5868            item.setAttribute("subscription", "remove");
5869            account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
5870        }
5871    }
5872
5873    public void updateConversation(final Conversation conversation) {
5874        mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation));
5875    }
5876
5877    private void reconnectAccount(
5878            final Account account, final boolean force, final boolean interactive) {
5879        synchronized (account) {
5880            final XmppConnection existingConnection = account.getXmppConnection();
5881            final XmppConnection connection;
5882            if (existingConnection != null) {
5883                connection = existingConnection;
5884            } else if (account.isConnectionEnabled()) {
5885                connection = createConnection(account);
5886                account.setXmppConnection(connection);
5887            } else {
5888                return;
5889            }
5890            final boolean hasInternet = hasInternetConnection();
5891            if (account.isConnectionEnabled() && hasInternet) {
5892                if (!force) {
5893                    disconnect(account, false);
5894                }
5895                Thread thread = new Thread(connection);
5896                connection.setInteractive(interactive);
5897                connection.prepareNewConnection();
5898                connection.interrupt();
5899                thread.start();
5900                scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
5901            } else {
5902                disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
5903                account.getRoster().clearPresences();
5904                connection.resetEverything();
5905                final AxolotlService axolotlService = account.getAxolotlService();
5906                if (axolotlService != null) {
5907                    axolotlService.resetBrokenness();
5908                }
5909                if (!hasInternet) {
5910                    account.setStatus(Account.State.NO_INTERNET);
5911                }
5912            }
5913        }
5914    }
5915
5916    public void reconnectAccountInBackground(final Account account) {
5917        new Thread(() -> reconnectAccount(account, false, true)).start();
5918    }
5919
5920    public void invite(final Conversation conversation, final Jid contact) {
5921        Log.d(
5922                Config.LOGTAG,
5923                conversation.getAccount().getJid().asBareJid()
5924                        + ": inviting "
5925                        + contact
5926                        + " to "
5927                        + conversation.getJid().asBareJid());
5928        final MucOptions.User user =
5929                conversation.getMucOptions().findUserByRealJid(contact.asBareJid());
5930        if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) {
5931            changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null);
5932        }
5933        final var packet = mMessageGenerator.invite(conversation, contact);
5934        sendMessagePacket(conversation.getAccount(), packet);
5935    }
5936
5937    public void directInvite(Conversation conversation, Jid jid) {
5938        final var packet = mMessageGenerator.directInvite(conversation, jid);
5939        sendMessagePacket(conversation.getAccount(), packet);
5940    }
5941
5942    public void resetSendingToWaiting(Account account) {
5943        for (Conversation conversation : getConversations()) {
5944            if (conversation.getAccount() == account) {
5945                conversation.findUnsentTextMessages(
5946                        message -> markMessage(message, Message.STATUS_WAITING));
5947            }
5948        }
5949    }
5950
5951    public Message markMessage(
5952            final Account account, final Jid recipient, final String uuid, final int status) {
5953        return markMessage(account, recipient, uuid, status, null);
5954    }
5955
5956    public Message markMessage(
5957            final Account account,
5958            final Jid recipient,
5959            final String uuid,
5960            final int status,
5961            String errorMessage) {
5962        if (uuid == null) {
5963            return null;
5964        }
5965        for (Conversation conversation : getConversations()) {
5966            if (conversation.getJid().asBareJid().equals(recipient)
5967                    && conversation.getAccount() == account) {
5968                final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
5969                if (message != null) {
5970                    markMessage(message, status, errorMessage);
5971                }
5972                return message;
5973            }
5974        }
5975        return null;
5976    }
5977
5978    public boolean markMessage(
5979            final Conversation conversation,
5980            final String uuid,
5981            final int status,
5982            final String serverMessageId) {
5983        return markMessage(conversation, uuid, status, serverMessageId, null, null, null, null, null);
5984    }
5985
5986    public boolean markMessage(final Conversation conversation, final String uuid, final int status, final String serverMessageId, final LocalizedContent body, final Element html, final String subject, final Element thread, final Set<Message.FileParams> attachments) {
5987        if (uuid == null) {
5988            return false;
5989        } else {
5990            final Message message = conversation.findSentMessageWithUuid(uuid);
5991            if (message != null) {
5992                if (message.getServerMsgId() == null) {
5993                    message.setServerMsgId(serverMessageId);
5994                }
5995                if (message.getEncryption() == Message.ENCRYPTION_NONE && (body != null || html != null || subject != null || thread != null || attachments != null)) {
5996                    message.setBody(body.content);
5997                    if (body.count > 1) {
5998                        message.setBodyLanguage(body.language);
5999                    }
6000                    message.setHtml(html);
6001                    message.setSubject(subject);
6002                    message.setThread(thread);
6003                    if (attachments != null && attachments.isEmpty()) {
6004                        message.setRelativeFilePath(null);
6005                        message.resetFileParams();
6006                    }
6007                    markMessage(message, status, null, true);
6008                } else {
6009                    markMessage(message, status);
6010                }
6011                return true;
6012            } else {
6013                return false;
6014            }
6015        }
6016    }
6017
6018    public void markMessage(Message message, int status) {
6019        markMessage(message, status, null);
6020    }
6021
6022    public void markMessage(final Message message, final int status, final String errorMessage) {
6023        markMessage(message, status, errorMessage, false);
6024    }
6025
6026    public void markMessage(
6027            final Message message,
6028            final int status,
6029            final String errorMessage,
6030            final boolean includeBody) {
6031        final int oldStatus = message.getStatus();
6032        if (status == Message.STATUS_SEND_FAILED
6033                && (oldStatus == Message.STATUS_SEND_RECEIVED
6034                        || oldStatus == Message.STATUS_SEND_DISPLAYED)) {
6035            return;
6036        }
6037        if (status == Message.STATUS_SEND_RECEIVED && oldStatus == Message.STATUS_SEND_DISPLAYED) {
6038            return;
6039        }
6040        message.setErrorMessage(errorMessage);
6041        message.setStatus(status);
6042        databaseBackend.updateMessage(message, includeBody);
6043        updateConversationUi();
6044        if (oldStatus != status && status == Message.STATUS_SEND_FAILED) {
6045            mNotificationService.pushFailedDelivery(message);
6046        }
6047    }
6048
6049    public SharedPreferences getPreferences() {
6050        return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
6051    }
6052
6053    public long getAutomaticMessageDeletionDate() {
6054        final long timeout =
6055                getLongPreference(
6056                        AppSettings.AUTOMATIC_MESSAGE_DELETION,
6057                        R.integer.automatic_message_deletion);
6058        return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000));
6059    }
6060
6061    public long getLongPreference(String name, @IntegerRes int res) {
6062        long defaultValue = getResources().getInteger(res);
6063        try {
6064            return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue)));
6065        } catch (NumberFormatException e) {
6066            return defaultValue;
6067        }
6068    }
6069
6070    public boolean getBooleanPreference(String name, @BoolRes int res) {
6071        return getPreferences().getBoolean(name, getResources().getBoolean(res));
6072    }
6073
6074    public String getStringPreference(String name, @BoolRes int res) {
6075        return getPreferences().getString(name, getResources().getString(res));
6076    }
6077
6078    public boolean confirmMessages() {
6079        return getBooleanPreference("confirm_messages", R.bool.confirm_messages);
6080    }
6081
6082    public boolean allowMessageCorrection() {
6083        return getBooleanPreference("allow_message_correction", R.bool.allow_message_correction);
6084    }
6085
6086    public boolean sendChatStates() {
6087        return getBooleanPreference("chat_states", R.bool.chat_states);
6088    }
6089
6090    public boolean useTorToConnect() {
6091        return getBooleanPreference("use_tor", R.bool.use_tor);
6092    }
6093
6094    public boolean broadcastLastActivity() {
6095        return getBooleanPreference(AppSettings.BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
6096    }
6097
6098    public int unreadCount() {
6099        int count = 0;
6100        for (Conversation conversation : getConversations()) {
6101            count += conversation.unreadCount(this);
6102        }
6103        return count;
6104    }
6105
6106    private <T> List<T> threadSafeList(Set<T> set) {
6107        synchronized (LISTENER_LOCK) {
6108            return set.isEmpty() ? Collections.emptyList() : new ArrayList<>(set);
6109        }
6110    }
6111
6112    public void showErrorToastInUi(int resId) {
6113        for (OnShowErrorToast listener : threadSafeList(this.mOnShowErrorToasts)) {
6114            listener.onShowErrorToast(resId);
6115        }
6116    }
6117
6118    public void updateConversationUi() {
6119        updateConversationUi(false);
6120    }
6121
6122    public void updateConversationUi(boolean newCaps) {
6123        for (OnConversationUpdate listener : threadSafeList(this.mOnConversationUpdates)) {
6124            listener.onConversationUpdate(newCaps);
6125        }
6126    }
6127
6128    public void notifyJingleRtpConnectionUpdate(
6129            final Account account,
6130            final Jid with,
6131            final String sessionId,
6132            final RtpEndUserState state) {
6133        for (OnJingleRtpConnectionUpdate listener :
6134                threadSafeList(this.onJingleRtpConnectionUpdate)) {
6135            listener.onJingleRtpConnectionUpdate(account, with, sessionId, state);
6136        }
6137    }
6138
6139    public void notifyJingleRtpConnectionUpdate(
6140            CallIntegration.AudioDevice selectedAudioDevice,
6141            Set<CallIntegration.AudioDevice> availableAudioDevices) {
6142        for (OnJingleRtpConnectionUpdate listener :
6143                threadSafeList(this.onJingleRtpConnectionUpdate)) {
6144            listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
6145        }
6146    }
6147
6148    public void updateAccountUi() {
6149        for (final OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) {
6150            listener.onAccountUpdate();
6151        }
6152    }
6153
6154    public void updateRosterUi(final UpdateRosterReason reason) {
6155        if (reason == UpdateRosterReason.PRESENCE) throw new IllegalArgumentException("PRESENCE must also come with a contact");
6156        updateRosterUi(reason, null);
6157    }
6158
6159    public void updateRosterUi(final UpdateRosterReason reason, final Contact contact) {
6160        for (OnRosterUpdate listener : threadSafeList(this.mOnRosterUpdates)) {
6161            listener.onRosterUpdate(reason, contact);
6162        }
6163    }
6164
6165    public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
6166        if (mOnCaptchaRequested.size() > 0) {
6167            DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
6168            Bitmap scaled =
6169                    Bitmap.createScaledBitmap(
6170                            captcha,
6171                            (int) (captcha.getWidth() * metrics.scaledDensity),
6172                            (int) (captcha.getHeight() * metrics.scaledDensity),
6173                            false);
6174            for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
6175                listener.onCaptchaRequested(account, id, data, scaled);
6176            }
6177            return true;
6178        }
6179        return false;
6180    }
6181
6182    public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
6183        for (OnUpdateBlocklist listener : threadSafeList(this.mOnUpdateBlocklist)) {
6184            listener.OnUpdateBlocklist(status);
6185        }
6186    }
6187
6188    public void updateMucRosterUi() {
6189        for (OnMucRosterUpdate listener : threadSafeList(this.mOnMucRosterUpdate)) {
6190            listener.onMucRosterUpdate();
6191        }
6192    }
6193
6194    public void keyStatusUpdated(AxolotlService.FetchStatus report) {
6195        for (OnKeyStatusUpdated listener : threadSafeList(this.mOnKeyStatusUpdated)) {
6196            listener.onKeyStatusUpdated(report);
6197        }
6198    }
6199
6200    public Account findAccountByJid(final Jid jid) {
6201        for (final Account account : this.accounts) {
6202            if (account.getJid().asBareJid().equals(jid.asBareJid())) {
6203                return account;
6204            }
6205        }
6206        return null;
6207    }
6208
6209    public Account findAccountByUuid(final String uuid) {
6210        for (Account account : this.accounts) {
6211            if (account.getUuid().equals(uuid)) {
6212                return account;
6213            }
6214        }
6215        return null;
6216    }
6217
6218    public Conversation findConversationByUuid(String uuid) {
6219        for (Conversation conversation : getConversations()) {
6220            if (conversation.getUuid().equals(uuid)) {
6221                return conversation;
6222            }
6223        }
6224        return null;
6225    }
6226
6227    public Conversation findUniqueConversationByJid(XmppUri xmppUri) {
6228        List<Conversation> findings = new ArrayList<>();
6229        for (Conversation c : getConversations()) {
6230            if (c.getAccount().isEnabled()
6231                    && c.getJid().asBareJid().equals(xmppUri.getJid().asBareJid())
6232                    && ((c.getMode() == Conversational.MODE_MULTI)
6233                            == xmppUri.isAction(XmppUri.ACTION_JOIN))) {
6234                findings.add(c);
6235            }
6236        }
6237        return findings.size() == 1 ? findings.get(0) : null;
6238    }
6239
6240    public boolean markRead(final Conversation conversation, boolean dismiss) {
6241        return markRead(conversation, null, dismiss).size() > 0;
6242    }
6243
6244    public void markRead(final Conversation conversation) {
6245        markRead(conversation, null, true);
6246    }
6247
6248    public List<Message> markRead(
6249            final Conversation conversation, String upToUuid, boolean dismiss) {
6250        if (dismiss) {
6251            mNotificationService.clear(conversation);
6252        }
6253        final List<Message> readMessages = conversation.markRead(upToUuid);
6254        if (readMessages.size() > 0) {
6255            Runnable runnable =
6256                    () -> {
6257                        for (Message message : readMessages) {
6258                            databaseBackend.updateMessage(message, false);
6259                        }
6260                    };
6261            mDatabaseWriterExecutor.execute(runnable);
6262            updateConversationUi();
6263            updateUnreadCountBadge();
6264            return readMessages;
6265        } else {
6266            return readMessages;
6267        }
6268    }
6269
6270    public void markNotificationDismissed(final List<Message> messages) {
6271        Runnable runnable = () -> {
6272            for (final var message : messages) {
6273                message.markNotificationDismissed();
6274                databaseBackend.updateMessage(message, false);
6275            }
6276        };
6277        mDatabaseWriterExecutor.execute(runnable);
6278    }
6279
6280    public synchronized void updateUnreadCountBadge() {
6281        int count = unreadCount();
6282        if (unreadCount != count) {
6283            Log.d(Config.LOGTAG, "update unread count to " + count);
6284            if (count > 0) {
6285                ShortcutBadger.applyCount(getApplicationContext(), count);
6286            } else {
6287                ShortcutBadger.removeCount(getApplicationContext());
6288            }
6289            unreadCount = count;
6290        }
6291    }
6292
6293    public void sendReadMarker(final Conversation conversation, final String upToUuid) {
6294        final boolean isPrivateAndNonAnonymousMuc =
6295                conversation.getMode() == Conversation.MODE_MULTI
6296                        && conversation.isPrivateAndNonAnonymous();
6297        final List<Message> readMessages = this.markRead(conversation, upToUuid, true);
6298        if (readMessages.isEmpty()) {
6299            return;
6300        }
6301        final var account = conversation.getAccount();
6302        final var connection = account.getXmppConnection();
6303        updateConversationUi();
6304        final var last =
6305                Iterables.getLast(
6306                        Collections2.filter(
6307                                readMessages,
6308                                m ->
6309                                        !m.isPrivateMessage()
6310                                                && m.getStatus() == Message.STATUS_RECEIVED),
6311                        null);
6312        if (last == null) {
6313            return;
6314        }
6315
6316        final boolean sendDisplayedMarker =
6317                confirmMessages()
6318                        && (last.trusted() || isPrivateAndNonAnonymousMuc)
6319                        && last.getRemoteMsgId() != null
6320                        && (last.markable || isPrivateAndNonAnonymousMuc);
6321        final boolean serverAssist =
6322                connection != null && connection.getFeatures().mdsServerAssist();
6323
6324        final String stanzaId = last.getServerMsgId();
6325
6326        if (sendDisplayedMarker && serverAssist) {
6327            final var mdsDisplayed = mIqGenerator.mdsDisplayed(stanzaId, conversation);
6328            final var packet = mMessageGenerator.confirm(last);
6329            packet.addChild(mdsDisplayed);
6330            if (!last.isPrivateMessage()) {
6331                packet.setTo(packet.getTo().asBareJid());
6332            }
6333            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server assisted " + packet);
6334            this.sendMessagePacket(account, packet);
6335        } else {
6336            publishMds(last);
6337            // read markers will be sent after MDS to flush the CSI stanza queue
6338            if (sendDisplayedMarker) {
6339                Log.d(
6340                        Config.LOGTAG,
6341                        conversation.getAccount().getJid().asBareJid()
6342                                + ": sending displayed marker to "
6343                                + last.getCounterpart().toString());
6344                final var packet = mMessageGenerator.confirm(last);
6345                this.sendMessagePacket(account, packet);
6346            }
6347        }
6348    }
6349
6350    private void publishMds(@Nullable final Message message) {
6351        final String stanzaId = message == null ? null : message.getServerMsgId();
6352        if (Strings.isNullOrEmpty(stanzaId)) {
6353            return;
6354        }
6355        final Conversation conversation;
6356        final var conversational = message.getConversation();
6357        if (conversational instanceof Conversation c) {
6358            conversation = c;
6359        } else {
6360            return;
6361        }
6362        final var account = conversation.getAccount();
6363        final var connection = account.getXmppConnection();
6364        if (connection == null || !connection.getFeatures().mds()) {
6365            return;
6366        }
6367        final Jid itemId;
6368        if (message.isPrivateMessage()) {
6369            itemId = message.getCounterpart();
6370        } else {
6371            itemId = conversation.getJid().asBareJid();
6372        }
6373        Log.d(Config.LOGTAG, "publishing mds for " + itemId + "/" + stanzaId);
6374        publishMds(account, itemId, stanzaId, conversation);
6375    }
6376
6377    private void publishMds(
6378            final Account account,
6379            final Jid itemId,
6380            final String stanzaId,
6381            final Conversation conversation) {
6382        final var item = mIqGenerator.mdsDisplayed(stanzaId, conversation);
6383        pushNodeAndEnforcePublishOptions(
6384                account,
6385                Namespace.MDS_DISPLAYED,
6386                item,
6387                itemId.toString(),
6388                PublishOptions.persistentWhitelistAccessMaxItems());
6389    }
6390
6391    public boolean sendReactions(final Message message, final Collection<String> reactions) {
6392        if (message.isPrivateMessage()) throw new IllegalArgumentException("Reactions to PM not implemented");
6393        if (message.getConversation() instanceof Conversation conversation) {
6394            final var isPrivateMessage = message.isPrivateMessage();
6395            final Jid reactTo;
6396            final boolean typeGroupChat;
6397            final String reactToId;
6398            final Collection<Reaction> combinedReactions;
6399            final var newReactions = new HashSet<>(reactions);
6400            newReactions.removeAll(message.getAggregatedReactions().ourReactions);
6401            if (conversation.getMode() == Conversational.MODE_MULTI && !isPrivateMessage) {
6402                final var mucOptions = conversation.getMucOptions();
6403                if (!mucOptions.participating()) {
6404                    Log.d(Config.LOGTAG, "not participating in MUC");
6405                    return false;
6406                }
6407                final var self = mucOptions.getSelf();
6408                final String occupantId = self.getOccupantId();
6409                if (Strings.isNullOrEmpty(occupantId)) {
6410                    Log.d(Config.LOGTAG, "occupant id not found for reaction in MUC");
6411                    return false;
6412                }
6413                final var existingRaw =
6414                        ImmutableSet.copyOf(
6415                                Collections2.transform(message.getReactions(), r -> r.reaction));
6416                final var reactionsAsExistingVariants =
6417                        ImmutableSet.copyOf(
6418                                Collections2.transform(
6419                                        reactions, r -> Emoticons.existingVariant(r, existingRaw)));
6420                if (!reactions.equals(reactionsAsExistingVariants)) {
6421                    Log.d(Config.LOGTAG, "modified reactions to existing variants");
6422                }
6423                reactToId = message.getServerMsgId();
6424                reactTo = conversation.getJid().asBareJid();
6425                typeGroupChat = true;
6426                combinedReactions =
6427                        Reaction.withMine(
6428                                message.getReactions(),
6429                                reactionsAsExistingVariants,
6430                                false,
6431                                self.getFullJid(),
6432                                conversation.getAccount().getJid(),
6433                                occupantId,
6434                                null);
6435            } else {
6436                if (message.isCarbon() || message.getStatus() == Message.STATUS_RECEIVED) {
6437                    reactToId = message.getRemoteMsgId();
6438                } else {
6439                    reactToId = message.getUuid();
6440                }
6441                typeGroupChat = false;
6442                if (isPrivateMessage) {
6443                    reactTo = message.getCounterpart();
6444                } else {
6445                    reactTo = conversation.getJid().asBareJid();
6446                }
6447                combinedReactions =
6448                        Reaction.withFrom(
6449                                message.getReactions(),
6450                                reactions,
6451                                false,
6452                                conversation.getAccount().getJid(),
6453                                null);
6454            }
6455            if (reactTo == null || Strings.isNullOrEmpty(reactToId)) {
6456                return false;
6457            }
6458
6459            final var packet =
6460                    mMessageGenerator.reaction(reactTo, typeGroupChat, message, reactToId, reactions);
6461
6462            final var quote = QuoteHelper.quote(MessageUtils.prepareQuote(message)) + "\n";
6463            final var body  = quote + String.join(" ", newReactions);
6464            if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && newReactions.size() > 0) {
6465                FILE_ATTACHMENT_EXECUTOR.execute(() -> {
6466                    XmppAxolotlMessage axolotlMessage = conversation.getAccount().getAxolotlService().encrypt(body, conversation);
6467                    packet.setAxolotlMessage(axolotlMessage.toElement());
6468                    packet.addChild("encryption", "urn:xmpp:eme:0")
6469                        .setAttribute("name", "OMEMO")
6470                        .setAttribute("namespace", AxolotlService.PEP_PREFIX);
6471                    sendMessagePacket(conversation.getAccount(), packet);
6472                    message.setReactions(combinedReactions);
6473                    updateMessage(message, false);
6474                });
6475            } else if (conversation.getNextEncryption() == Message.ENCRYPTION_NONE || newReactions.size() < 1) {
6476                if (newReactions.size() > 0) {
6477                    packet.setBody(body);
6478
6479                    packet.addChild("reply", "urn:xmpp:reply:0")
6480                        .setAttribute("to", message.getCounterpart())
6481                        .setAttribute("id", reactToId);
6482                    final var replyFallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reply:0");
6483                    replyFallback.addChild("body", "urn:xmpp:fallback:0")
6484                        .setAttribute("start", "0")
6485                        .setAttribute("end", "" + quote.codePointCount(0, quote.length()));
6486
6487                    final var fallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reactions:0");
6488                    fallback.addChild("body", "urn:xmpp:fallback:0");
6489                }
6490
6491                sendMessagePacket(conversation.getAccount(), packet);
6492                message.setReactions(combinedReactions);
6493                updateMessage(message, false);
6494            }
6495
6496            return true;
6497        } else {
6498            return false;
6499        }
6500    }
6501
6502    public MemorizingTrustManager getMemorizingTrustManager() {
6503        return this.mMemorizingTrustManager;
6504    }
6505
6506    public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
6507        this.mMemorizingTrustManager = trustManager;
6508    }
6509
6510    public void updateMemorizingTrustManager() {
6511        final MemorizingTrustManager trustManager;
6512        if (appSettings.isTrustSystemCAStore()) {
6513            trustManager = new MemorizingTrustManager(getApplicationContext());
6514        } else {
6515            trustManager = new MemorizingTrustManager(getApplicationContext(), null);
6516        }
6517        setMemorizingTrustManager(trustManager);
6518    }
6519
6520    public LruCache<String, Drawable> getDrawableCache() {
6521        return this.mDrawableCache;
6522    }
6523
6524    public Collection<String> getKnownHosts() {
6525        final Set<String> hosts = new HashSet<>();
6526        for (final Account account : getAccounts()) {
6527            hosts.add(account.getServer());
6528            for (final Contact contact : account.getRoster().getContacts()) {
6529                if (contact.showInRoster()) {
6530                    final String server = contact.getServer();
6531                    if (server != null) {
6532                        hosts.add(server);
6533                    }
6534                }
6535            }
6536        }
6537        if (Config.QUICKSY_DOMAIN != null) {
6538            hosts.remove(
6539                    Config.QUICKSY_DOMAIN
6540                            .toString()); // we only want to show this when we type a e164
6541            // number
6542        }
6543        if (Config.MAGIC_CREATE_DOMAIN != null) {
6544            hosts.add(Config.MAGIC_CREATE_DOMAIN);
6545        }
6546        hosts.add("chat.above.im");
6547        return hosts;
6548    }
6549
6550    public Collection<String> getKnownConferenceHosts() {
6551        final Set<String> mucServers = new HashSet<>();
6552        for (final Account account : accounts) {
6553            if (account.getXmppConnection() != null) {
6554                mucServers.addAll(account.getXmppConnection().getMucServers());
6555                for (final Bookmark bookmark : account.getBookmarks()) {
6556                    final Jid jid = bookmark.getJid();
6557                    final String s = jid == null ? null : jid.getDomain().toString();
6558                    if (s != null) {
6559                        mucServers.add(s);
6560                    }
6561                }
6562            }
6563        }
6564        return mucServers;
6565    }
6566
6567    public void sendMessagePacket(
6568            final Account account,
6569            final im.conversations.android.xmpp.model.stanza.Message packet) {
6570        final XmppConnection connection = account.getXmppConnection();
6571        if (connection != null) {
6572            connection.sendMessagePacket(packet);
6573        }
6574    }
6575
6576    public void sendPresencePacket(
6577            final Account account,
6578            final im.conversations.android.xmpp.model.stanza.Presence packet) {
6579        final XmppConnection connection = account.getXmppConnection();
6580        if (connection != null) {
6581            connection.sendPresencePacket(packet);
6582        }
6583    }
6584
6585    public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
6586        final XmppConnection connection = account.getXmppConnection();
6587        if (connection == null) {
6588            return;
6589        }
6590        connection.sendCreateAccountWithCaptchaPacket(id, data);
6591    }
6592
6593    public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback) {
6594        sendIqPacket(account, packet, callback, null);
6595    }
6596
6597    public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback, Long timeout) {
6598        final XmppConnection connection = account.getXmppConnection();
6599        if (connection != null) {
6600            connection.sendIqPacket(packet, callback, timeout);
6601        } else if (callback != null) {
6602            callback.accept(Iq.TIMEOUT);
6603        }
6604    }
6605
6606    public void sendPresence(final Account account) {
6607        sendPresence(account, checkListeners() && broadcastLastActivity());
6608    }
6609
6610    private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
6611        final Presence.Status status;
6612        if (manuallyChangePresence()) {
6613            status = account.getPresenceStatus();
6614        } else {
6615            status = getTargetPresence();
6616        }
6617        final var packet = mPresenceGenerator.selfPresence(account, status);
6618        if (mLastActivity > 0 && includeIdleTimestamp) {
6619            long since =
6620                    Math.min(mLastActivity, System.currentTimeMillis()); // don't send future dates
6621            packet.addChild("idle", Namespace.IDLE)
6622                    .setAttribute("since", AbstractGenerator.getTimestamp(since));
6623        }
6624        sendPresencePacket(account, packet);
6625    }
6626
6627    private void deactivateGracePeriod() {
6628        for (Account account : getAccounts()) {
6629            account.deactivateGracePeriod();
6630        }
6631    }
6632
6633    public void refreshAllPresences() {
6634        boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
6635        for (Account account : getAccounts()) {
6636            if (account.isConnectionEnabled()) {
6637                sendPresence(account, includeIdleTimestamp);
6638            }
6639        }
6640    }
6641
6642    private void refreshAllFcmTokens() {
6643        for (Account account : getAccounts()) {
6644            if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
6645                mPushManagementService.registerPushTokenOnServer(account);
6646            }
6647        }
6648    }
6649
6650    private void sendOfflinePresence(final Account account) {
6651        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence");
6652        sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
6653    }
6654
6655    public MessageGenerator getMessageGenerator() {
6656        return this.mMessageGenerator;
6657    }
6658
6659    public PresenceGenerator getPresenceGenerator() {
6660        return this.mPresenceGenerator;
6661    }
6662
6663    public IqGenerator getIqGenerator() {
6664        return this.mIqGenerator;
6665    }
6666
6667    public JingleConnectionManager getJingleConnectionManager() {
6668        return this.mJingleConnectionManager;
6669    }
6670
6671    private boolean hasJingleRtpConnection(final Account account) {
6672        return this.mJingleConnectionManager.hasJingleRtpConnection(account);
6673    }
6674
6675    public MessageArchiveService getMessageArchiveService() {
6676        return this.mMessageArchiveService;
6677    }
6678
6679    public QuickConversationsService getQuickConversationsService() {
6680        return this.mQuickConversationsService;
6681    }
6682
6683    public List<Contact> findContacts(Jid jid, String accountJid) {
6684        ArrayList<Contact> contacts = new ArrayList<>();
6685        for (Account account : getAccounts()) {
6686            if ((account.isEnabled() || accountJid != null)
6687                    && (accountJid == null
6688                            || accountJid.equals(account.getJid().asBareJid().toString()))) {
6689                Contact contact = account.getRoster().getContactFromContactList(jid);
6690                if (contact != null) {
6691                    contacts.add(contact);
6692                }
6693            }
6694        }
6695        return contacts;
6696    }
6697
6698    public Conversation findFirstMuc(Jid jid) {
6699        return findFirstMuc(jid, null);
6700    }
6701
6702    public Conversation findFirstMuc(Jid jid, String accountJid) {
6703        for (Conversation conversation : getConversations()) {
6704            if ((conversation.getAccount().isEnabled() || accountJid != null)
6705                    && (accountJid == null || accountJid.equals(conversation.getAccount().getJid().asBareJid().toString()))
6706                    && conversation.getJid().asBareJid().equals(jid.asBareJid()) && conversation.getMode() == Conversation.MODE_MULTI) {
6707                return conversation;
6708            }
6709        }
6710        return null;
6711    }
6712
6713    public NotificationService getNotificationService() {
6714        return this.mNotificationService;
6715    }
6716
6717    public HttpConnectionManager getHttpConnectionManager() {
6718        return this.mHttpConnectionManager;
6719    }
6720
6721    public void resendFailedMessages(final Message message, final boolean forceP2P) {
6722        message.setTime(System.currentTimeMillis());
6723        markMessage(message, Message.STATUS_WAITING);
6724        this.sendMessage(message, true, false, false, forceP2P, null);
6725        if (message.getConversation() instanceof Conversation c) {
6726            c.sort();
6727        }
6728        updateConversationUi();
6729    }
6730
6731    public void clearConversationHistory(final Conversation conversation) {
6732        final long clearDate;
6733        final String reference;
6734        if (conversation.countMessages() > 0) {
6735            Message latestMessage = conversation.getLatestMessage();
6736            clearDate = latestMessage.getTimeSent() + 1000;
6737            reference = latestMessage.getServerMsgId();
6738        } else {
6739            clearDate = System.currentTimeMillis();
6740            reference = null;
6741        }
6742        conversation.clearMessages();
6743        conversation.setHasMessagesLeftOnServer(false); // avoid messages getting loaded through mam
6744        conversation.setLastClearHistory(clearDate, reference);
6745        Runnable runnable =
6746                () -> {
6747                    databaseBackend.deleteMessagesInConversation(conversation);
6748                    databaseBackend.updateConversation(conversation);
6749                };
6750        mDatabaseWriterExecutor.execute(runnable);
6751    }
6752
6753    public boolean sendBlockRequest(
6754            final Blockable blockable, final boolean reportSpam, final String serverMsgId) {
6755        if (blockable != null && blockable.getBlockedJid() != null) {
6756            final var account = blockable.getAccount();
6757            final Jid jid = blockable.getBlockedJid();
6758            this.sendIqPacket(
6759                    account,
6760                    getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId),
6761                    (response) -> {
6762                        if (response.getType() == Iq.Type.RESULT) {
6763                            account.getBlocklist().add(jid);
6764                            updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
6765                        }
6766                    });
6767            if (blockable.getBlockedJid().isFullJid()) {
6768                return false;
6769            } else if (removeBlockedConversations(blockable.getAccount(), jid)) {
6770                updateConversationUi();
6771                return true;
6772            } else {
6773                return false;
6774            }
6775        } else {
6776            return false;
6777        }
6778    }
6779
6780    public boolean removeBlockedConversations(final Account account, final Jid blockedJid) {
6781        boolean removed = false;
6782        synchronized (this.conversations) {
6783            boolean domainJid = blockedJid.getLocal() == null;
6784            for (Conversation conversation : this.conversations) {
6785                boolean jidMatches =
6786                        (domainJid
6787                                        && blockedJid
6788                                                .getDomain()
6789                                                .equals(conversation.getJid().getDomain()))
6790                                || blockedJid.equals(conversation.getJid().asBareJid());
6791                if (conversation.getAccount() == account
6792                        && conversation.getMode() == Conversation.MODE_SINGLE
6793                        && jidMatches) {
6794                    this.conversations.remove(conversation);
6795                    markRead(conversation);
6796                    conversation.setStatus(Conversation.STATUS_ARCHIVED);
6797                    Log.d(
6798                            Config.LOGTAG,
6799                            account.getJid().asBareJid()
6800                                    + ": archiving conversation "
6801                                    + conversation.getJid().asBareJid()
6802                                    + " because jid was blocked");
6803                    updateConversation(conversation);
6804                    removed = true;
6805                }
6806            }
6807        }
6808        return removed;
6809    }
6810
6811    public void sendUnblockRequest(final Blockable blockable) {
6812        if (blockable != null && blockable.getJid() != null) {
6813            final var account = blockable.getAccount();
6814            final Jid jid = blockable.getBlockedJid();
6815            this.sendIqPacket(
6816                    account,
6817                    getIqGenerator().generateSetUnblockRequest(jid),
6818                    response -> {
6819                        if (response.getType() == Iq.Type.RESULT) {
6820                            account.getBlocklist().remove(jid);
6821                            updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
6822                        }
6823                    });
6824        }
6825    }
6826
6827    public void publishDisplayName(final Account account) {
6828        String displayName = account.getDisplayName();
6829        final Iq request;
6830        if (TextUtils.isEmpty(displayName)) {
6831            request = mIqGenerator.deleteNode(Namespace.NICK);
6832        } else {
6833            request = mIqGenerator.publishNick(displayName);
6834        }
6835        mAvatarService.clear(account);
6836        sendIqPacket(
6837                account,
6838                request,
6839                (packet) -> {
6840                    if (packet.getType() == Iq.Type.ERROR) {
6841                        Log.d(
6842                                Config.LOGTAG,
6843                                account.getJid().asBareJid()
6844                                        + ": unable to modify nick name "
6845                                        + packet);
6846                    }
6847                });
6848    }
6849
6850    public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String, String> key) {
6851        ServiceDiscoveryResult result = discoCache.get(key);
6852        if (result != null) {
6853            return result;
6854        } else {
6855            if (key.first == null || key.second == null) return null;
6856            result = databaseBackend.findDiscoveryResult(key.first, key.second);
6857            if (result != null) {
6858                discoCache.put(key, result);
6859            }
6860            return result;
6861        }
6862    }
6863
6864    public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) {
6865        final var request = new Iq(input == null ? Iq.Type.GET : Iq.Type.SET);
6866        request.setTo(jid);
6867        Element query = request.query("jabber:iq:gateway");
6868        if (input != null) {
6869            Element prompt = query.addChild("prompt");
6870            prompt.setContent(input);
6871        }
6872        sendIqPacket(account, request, packet -> {
6873            if (packet.getType() == Iq.Type.RESULT) {
6874                callback.onGatewayResult(packet.query().findChildContent(input == null ? "prompt" : "jid"), null);
6875            } else {
6876                Element error = packet.findChild("error");
6877                callback.onGatewayResult(null, error == null ? null : error.findChildContent("text"));
6878            }
6879        });
6880    }
6881
6882    public void fetchCaps(Account account, final Jid jid, final Presence presence) {
6883        fetchCaps(account, jid, presence, null);
6884    }
6885
6886    public void fetchCaps(Account account, final Jid jid, final Presence presence, Runnable cb) {
6887        final Pair<String, String> key = presence == null ? null : new Pair<>(presence.getHash(), presence.getVer());
6888        final ServiceDiscoveryResult disco = key == null ? null : getCachedServiceDiscoveryResult(key);
6889
6890        if (disco != null) {
6891            presence.setServiceDiscoveryResult(disco);
6892            final Contact contact = account.getRoster().getContact(jid);
6893            if (contact.refreshRtpCapability()) {
6894                syncRoster(account);
6895            }
6896            contact.refreshCaps();
6897            if (disco.hasIdentity("gateway", "pstn")) {
6898                contact.registerAsPhoneAccount(this);
6899                mQuickConversationsService.considerSyncBackground(false);
6900            }
6901            updateConversationUi(true);
6902        } else {
6903            final Iq request = new Iq(Iq.Type.GET);
6904            request.setTo(jid);
6905            final String node = presence == null ? null : presence.getNode();
6906            final String ver = presence == null ? null : presence.getVer();
6907            final Element query = request.query(Namespace.DISCO_INFO);
6908            if (node != null && ver != null) {
6909                query.setAttribute("node", node + "#" + ver);
6910            }
6911
6912            Log.d(
6913                    Config.LOGTAG,
6914                    account.getJid().asBareJid()
6915                            + ": making disco request for "
6916                            + (key == null ? null : key.second)
6917                            + " to "
6918                            + jid);
6919            sendIqPacket(
6920                    account,
6921                    request,
6922                    (response) -> {
6923                        if (response.getType() == Iq.Type.RESULT) {
6924                            final ServiceDiscoveryResult discoveryResult =
6925                                    new ServiceDiscoveryResult(response);
6926                            if (presence == null || presence.getVer() == null || presence.getVer().equals(discoveryResult.getVer())) {
6927                                databaseBackend.insertDiscoveryResult(discoveryResult);
6928                                injectServiceDiscoveryResult(
6929                                        account.getRoster(),
6930                                        presence == null ? null : presence.getHash(),
6931                                        presence == null ? null : presence.getVer(),
6932                                        jid.getResource(),
6933                                        discoveryResult);
6934                                if (discoveryResult.hasIdentity("gateway", "pstn")) {
6935                                    final Contact contact = account.getRoster().getContact(jid);
6936                                    contact.registerAsPhoneAccount(this);
6937                                    mQuickConversationsService.considerSyncBackground(false);
6938                                }
6939                                updateConversationUi(true);
6940                                if (cb != null) cb.run();
6941                            } else {
6942                                Log.d(
6943                                        Config.LOGTAG,
6944                                        account.getJid().asBareJid()
6945                                                + ": mismatch in caps for contact "
6946                                                + jid
6947                                                + " "
6948                                                + presence.getVer()
6949                                                + " vs "
6950                                                + discoveryResult.getVer());
6951                            }
6952                        } else {
6953                            Log.d(
6954                                    Config.LOGTAG,
6955                                    account.getJid().asBareJid()
6956                                            + ": unable to fetch caps from "
6957                                            + jid);
6958                        }
6959                    });
6960        }
6961    }
6962
6963    public void fetchCommands(Account account, final Jid jid, Consumer<Iq> callback) {
6964        final var request = mIqGenerator.queryDiscoItems(jid, "http://jabber.org/protocol/commands");
6965        sendIqPacket(account, request, callback);
6966    }
6967
6968    private void injectServiceDiscoveryResult(
6969            Roster roster, String hash, String ver, String resource, ServiceDiscoveryResult disco) {
6970        boolean rosterNeedsSync = false;
6971        for (final Contact contact : roster.getContacts()) {
6972            boolean serviceDiscoverySet = false;
6973            Presence onePresence = contact.getPresences().get(resource == null ? "" : resource);
6974            if (onePresence != null) {
6975                onePresence.setServiceDiscoveryResult(disco);
6976                serviceDiscoverySet = true;
6977            } else if (resource == null && hash == null && ver == null) {
6978                Presence p = new Presence(Presence.Status.OFFLINE, null, null, null, "");
6979                p.setServiceDiscoveryResult(disco);
6980                contact.updatePresence("", p);
6981                serviceDiscoverySet = true;
6982            }
6983            if (hash != null && ver != null) {
6984                for (final Presence presence : contact.getPresences().getPresences()) {
6985                    if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
6986                        presence.setServiceDiscoveryResult(disco);
6987                        serviceDiscoverySet = true;
6988                    }
6989                }
6990            }
6991            if (serviceDiscoverySet) {
6992                rosterNeedsSync |= contact.refreshRtpCapability();
6993                contact.refreshCaps();
6994            }
6995        }
6996        if (rosterNeedsSync) {
6997            syncRoster(roster.getAccount());
6998        }
6999    }
7000
7001    public void fetchMamPreferences(final Account account, final OnMamPreferencesFetched callback) {
7002        final MessageArchiveService.Version version = MessageArchiveService.Version.get(account);
7003        final Iq request = new Iq(Iq.Type.GET);
7004        request.addChild("prefs", version.namespace);
7005        sendIqPacket(
7006                account,
7007                request,
7008                (packet) -> {
7009                    final Element prefs = packet.findChild("prefs", version.namespace);
7010                    if (packet.getType() == Iq.Type.RESULT && prefs != null) {
7011                        callback.onPreferencesFetched(prefs);
7012                    } else {
7013                        callback.onPreferencesFetchFailed();
7014                    }
7015                });
7016    }
7017
7018    public PushManagementService getPushManagementService() {
7019        return mPushManagementService;
7020    }
7021
7022    public void changeStatus(Account account, PresenceTemplate template, String signature) {
7023        if (!template.getStatusMessage().isEmpty()) {
7024            databaseBackend.insertPresenceTemplate(template);
7025        }
7026        account.setPgpSignature(signature);
7027        account.setPresenceStatus(template.getStatus());
7028        account.setPresenceStatusMessage(template.getStatusMessage());
7029        databaseBackend.updateAccount(account);
7030        sendPresence(account);
7031    }
7032
7033    public List<PresenceTemplate> getPresenceTemplates(Account account) {
7034        List<PresenceTemplate> templates = databaseBackend.getPresenceTemplates();
7035        for (PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) {
7036            if (!templates.contains(template)) {
7037                templates.add(0, template);
7038            }
7039        }
7040        return templates;
7041    }
7042
7043    public void saveConversationAsBookmark(final Conversation conversation, final String name) {
7044        final Account account = conversation.getAccount();
7045        final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid());
7046        String nick = conversation.getMucOptions().getActualNick();
7047        if (nick == null) nick = conversation.getJid().getResource();
7048        if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
7049            bookmark.setNick(nick);
7050        }
7051        if (!TextUtils.isEmpty(name)) {
7052            bookmark.setBookmarkName(name);
7053        }
7054        bookmark.setAutojoin(true);
7055        createBookmark(account, bookmark);
7056        bookmark.setConversation(conversation);
7057    }
7058
7059    public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
7060        boolean performedVerification = false;
7061        final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
7062        for (XmppUri.Fingerprint fp : fingerprints) {
7063            if (fp.type == XmppUri.FingerprintType.OMEMO) {
7064                String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
7065                FingerprintStatus fingerprintStatus =
7066                        axolotlService.getFingerprintTrust(fingerprint);
7067                if (fingerprintStatus != null) {
7068                    if (!fingerprintStatus.isVerified()) {
7069                        performedVerification = true;
7070                        axolotlService.setFingerprintTrust(
7071                                fingerprint, fingerprintStatus.toVerified());
7072                    }
7073                } else {
7074                    axolotlService.preVerifyFingerprint(contact, fingerprint);
7075                }
7076            }
7077        }
7078        return performedVerification;
7079    }
7080
7081    public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
7082        final AxolotlService axolotlService = account.getAxolotlService();
7083        boolean verifiedSomething = false;
7084        for (XmppUri.Fingerprint fp : fingerprints) {
7085            if (fp.type == XmppUri.FingerprintType.OMEMO) {
7086                String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
7087                Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint);
7088                FingerprintStatus fingerprintStatus =
7089                        axolotlService.getFingerprintTrust(fingerprint);
7090                if (fingerprintStatus != null) {
7091                    if (!fingerprintStatus.isVerified()) {
7092                        axolotlService.setFingerprintTrust(
7093                                fingerprint, fingerprintStatus.toVerified());
7094                        verifiedSomething = true;
7095                    }
7096                } else {
7097                    axolotlService.preVerifyFingerprint(account, fingerprint);
7098                    verifiedSomething = true;
7099                }
7100            }
7101        }
7102        return verifiedSomething;
7103    }
7104
7105    public boolean blindTrustBeforeVerification() {
7106        return getBooleanPreference(AppSettings.BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv);
7107    }
7108
7109    public ShortcutService getShortcutService() {
7110        return mShortcutService;
7111    }
7112
7113    public void pushMamPreferences(Account account, Element prefs) {
7114        final Iq set = new Iq(Iq.Type.SET);
7115        set.addChild(prefs);
7116        account.setMamPrefs(prefs);
7117        sendIqPacket(account, set, null);
7118    }
7119
7120    public void evictPreview(File f) {
7121        if (f == null) return;
7122
7123        if (mDrawableCache.remove(f.getAbsolutePath()) != null) {
7124            Log.d(Config.LOGTAG, "deleted cached preview");
7125        }
7126    }
7127
7128    public void evictPreview(String uuid) {
7129        if (mDrawableCache.remove(uuid) != null) {
7130            Log.d(Config.LOGTAG, "deleted cached preview");
7131        }
7132    }
7133
7134    public interface OnMamPreferencesFetched {
7135        void onPreferencesFetched(Element prefs);
7136
7137        void onPreferencesFetchFailed();
7138    }
7139
7140    public interface OnAccountCreated {
7141        void onAccountCreated(Account account);
7142
7143        void informUser(int r);
7144    }
7145
7146    public interface OnMoreMessagesLoaded {
7147        void onMoreMessagesLoaded(int count, Conversation conversation);
7148
7149        void informUser(int r);
7150    }
7151
7152    public interface OnAccountPasswordChanged {
7153        void onPasswordChangeSucceeded();
7154
7155        void onPasswordChangeFailed();
7156    }
7157
7158    public interface OnRoomDestroy {
7159        void onRoomDestroySucceeded();
7160
7161        void onRoomDestroyFailed();
7162    }
7163
7164    public interface OnAffiliationChanged {
7165        void onAffiliationChangedSuccessful(Jid jid);
7166
7167        void onAffiliationChangeFailed(Jid jid, int resId);
7168    }
7169
7170    public interface OnConversationUpdate {
7171        default void onConversationUpdate() { onConversationUpdate(false); }
7172        default void onConversationUpdate(boolean newCaps) { onConversationUpdate(); }
7173    }
7174
7175    public interface OnJingleRtpConnectionUpdate {
7176        void onJingleRtpConnectionUpdate(
7177                final Account account,
7178                final Jid with,
7179                final String sessionId,
7180                final RtpEndUserState state);
7181
7182        void onAudioDeviceChanged(
7183                CallIntegration.AudioDevice selectedAudioDevice,
7184                Set<CallIntegration.AudioDevice> availableAudioDevices);
7185    }
7186
7187    public interface OnAccountUpdate {
7188        void onAccountUpdate();
7189    }
7190
7191    public interface OnCaptchaRequested {
7192        void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha);
7193    }
7194
7195    public interface OnRosterUpdate {
7196        void onRosterUpdate(final UpdateRosterReason reason, final Contact contact);
7197    }
7198
7199    public interface OnMucRosterUpdate {
7200        void onMucRosterUpdate();
7201    }
7202
7203    public interface OnConferenceConfigurationFetched {
7204        void onConferenceConfigurationFetched(Conversation conversation);
7205
7206        void onFetchFailed(Conversation conversation, String errorCondition);
7207    }
7208
7209    public interface OnConferenceJoined {
7210        void onConferenceJoined(Conversation conversation);
7211    }
7212
7213    public interface OnConfigurationPushed {
7214        void onPushSucceeded();
7215
7216        void onPushFailed();
7217    }
7218
7219    public interface OnShowErrorToast {
7220        void onShowErrorToast(int resId);
7221    }
7222
7223    public class XmppConnectionBinder extends Binder {
7224        public XmppConnectionService getService() {
7225            return XmppConnectionService.this;
7226        }
7227    }
7228
7229    private class InternalEventReceiver extends BroadcastReceiver {
7230
7231        @Override
7232        public void onReceive(final Context context, final Intent intent) {
7233            onStartCommand(intent, 0, 0);
7234        }
7235    }
7236
7237    private class RestrictedEventReceiver extends BroadcastReceiver {
7238
7239        private final Collection<String> allowedActions;
7240
7241        private RestrictedEventReceiver(final Collection<String> allowedActions) {
7242            this.allowedActions = allowedActions;
7243        }
7244
7245        @Override
7246        public void onReceive(final Context context, final Intent intent) {
7247            final String action = intent == null ? null : intent.getAction();
7248            if (allowedActions.contains(action)) {
7249                onStartCommand(intent, 0, 0);
7250            } else {
7251                Log.e(Config.LOGTAG, "restricting broadcast of event " + action);
7252            }
7253        }
7254    }
7255
7256    public static class OngoingCall {
7257        public final AbstractJingleConnection.Id id;
7258        public final Set<Media> media;
7259        public final boolean reconnecting;
7260
7261        public OngoingCall(
7262                AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
7263            this.id = id;
7264            this.media = media;
7265            this.reconnecting = reconnecting;
7266        }
7267
7268        @Override
7269        public boolean equals(Object o) {
7270            if (this == o) return true;
7271            if (o == null || getClass() != o.getClass()) return false;
7272            OngoingCall that = (OngoingCall) o;
7273            return reconnecting == that.reconnecting
7274                    && Objects.equal(id, that.id)
7275                    && Objects.equal(media, that.media);
7276        }
7277
7278        @Override
7279        public int hashCode() {
7280            return Objects.hashCode(id, media, reconnecting);
7281        }
7282    }
7283
7284    public static void toggleForegroundService(final XmppConnectionService service) {
7285        if (service == null) {
7286            return;
7287        }
7288        service.toggleForegroundService();
7289    }
7290
7291    public static void toggleForegroundService(final ConversationsActivity activity) {
7292        if (activity == null) {
7293            return;
7294        }
7295        toggleForegroundService(activity.xmppConnectionService);
7296    }
7297
7298    public static class BlockedMediaException extends Exception { }
7299
7300    public static enum UpdateRosterReason {
7301        INIT,
7302        AVATAR,
7303        PUSH,
7304        PRESENCE
7305    }
7306}