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