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