XmppConnectionService.java

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