XmppConnectionService.java

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