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, XmppConnection> 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        // TODO use PingManager
4180        final Jid self = conversation.getMucOptions().getSelf().getFullJid();
4181        final Iq ping = new Iq(Iq.Type.GET);
4182        ping.setTo(self);
4183        ping.addChild("ping", Namespace.PING);
4184        sendIqPacket(
4185                conversation.getAccount(),
4186                ping,
4187                (response) -> {
4188                    if (response.getType() == Iq.Type.ERROR) {
4189                        final var error = response.getError();
4190                        if (error == null
4191                                || error.hasChild("service-unavailable")
4192                                || error.hasChild("feature-not-implemented")
4193                                || error.hasChild("item-not-found")) {
4194                            Log.d(
4195                                    Config.LOGTAG,
4196                                    account.getJid().asBareJid()
4197                                            + ": ping to "
4198                                            + self
4199                                            + " came back as ignorable error");
4200                        } else {
4201                            Log.d(
4202                                    Config.LOGTAG,
4203                                    account.getJid().asBareJid()
4204                                            + ": ping to "
4205                                            + self
4206                                            + " failed. attempting rejoin");
4207                            joinMuc(conversation);
4208                        }
4209                    } else if (response.getType() == Iq.Type.RESULT) {
4210                        Log.d(
4211                                Config.LOGTAG,
4212                                account.getJid().asBareJid()
4213                                        + ": ping to "
4214                                        + self
4215                                        + " came back fine");
4216                    }
4217                    synchronized (account.inProgressConferencePings) {
4218                        account.inProgressConferencePings.remove(conversation);
4219                    }
4220                });
4221    }
4222
4223    public void joinMuc(Conversation conversation) {
4224        joinMuc(conversation, null, false);
4225    }
4226
4227    public void joinMuc(Conversation conversation, boolean followedInvite) {
4228        joinMuc(conversation, null, followedInvite);
4229    }
4230
4231    private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
4232        joinMuc(conversation, onConferenceJoined, false);
4233    }
4234
4235    private void joinMuc(
4236            final Conversation conversation,
4237            final OnConferenceJoined onConferenceJoined,
4238            final boolean followedInvite) {
4239        final Account account = conversation.getAccount();
4240        synchronized (account.pendingConferenceJoins) {
4241            account.pendingConferenceJoins.remove(conversation);
4242        }
4243        synchronized (account.pendingConferenceLeaves) {
4244            account.pendingConferenceLeaves.remove(conversation);
4245        }
4246        if (account.getStatus() == Account.State.ONLINE) {
4247            synchronized (account.inProgressConferenceJoins) {
4248                account.inProgressConferenceJoins.add(conversation);
4249            }
4250            if (Config.MUC_LEAVE_BEFORE_JOIN) {
4251                sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions()));
4252            }
4253            conversation.resetMucOptions();
4254            if (onConferenceJoined != null) {
4255                conversation.getMucOptions().flagNoAutoPushConfiguration();
4256            }
4257            conversation.setHasMessagesLeftOnServer(false);
4258            fetchConferenceConfiguration(
4259                    conversation,
4260                    new OnConferenceConfigurationFetched() {
4261
4262                        private void join(Conversation conversation) {
4263                            Account account = conversation.getAccount();
4264                            final MucOptions mucOptions = conversation.getMucOptions();
4265
4266                            if (mucOptions.nonanonymous()
4267                                    && !mucOptions.membersOnly()
4268                                    && !conversation.getBooleanAttribute(
4269                                            "accept_non_anonymous", false)) {
4270                                synchronized (account.inProgressConferenceJoins) {
4271                                    account.inProgressConferenceJoins.remove(conversation);
4272                                }
4273                                mucOptions.setError(MucOptions.Error.NON_ANONYMOUS);
4274                                updateConversationUi();
4275                                if (onConferenceJoined != null) {
4276                                    onConferenceJoined.onConferenceJoined(conversation);
4277                                }
4278                                return;
4279                            }
4280
4281                            final Jid joinJid = mucOptions.getSelf().getFullJid();
4282                            Log.d(
4283                                    Config.LOGTAG,
4284                                    account.getJid().asBareJid().toString()
4285                                            + ": joining conversation "
4286                                            + joinJid.toString());
4287                            final var packet =
4288                                    mPresenceGenerator.selfPresence(
4289                                            account,
4290                                            im.conversations.android.xmpp.model.stanza.Presence
4291                                                    .Availability.ONLINE,
4292                                            mucOptions.nonanonymous()
4293                                                    || onConferenceJoined != null,
4294                                            mucOptions.getSelf().getNick());
4295                            packet.setTo(joinJid);
4296                            Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
4297                            if (conversation.getMucOptions().getPassword() != null) {
4298                                x.addChild("password").setContent(mucOptions.getPassword());
4299                            }
4300
4301                            if (mucOptions.mamSupport()) {
4302                                // Use MAM instead of the limited muc history to get history
4303                                x.addChild("history").setAttribute("maxchars", "0");
4304                            } else {
4305                                // Fallback to muc history
4306                                x.addChild("history")
4307                                        .setAttribute(
4308                                                "since",
4309                                                PresenceGenerator.getTimestamp(
4310                                                        conversation
4311                                                                .getLastMessageTransmitted()
4312                                                                .getTimestamp()));
4313                            }
4314                            sendPresencePacket(account, packet);
4315                            if (onConferenceJoined != null) {
4316                                onConferenceJoined.onConferenceJoined(conversation);
4317                            }
4318                            if (!joinJid.equals(conversation.getJid())) {
4319                                conversation.setContactJid(joinJid);
4320                                databaseBackend.updateConversation(conversation);
4321                            }
4322
4323                            maybeRegisterWithMuc(conversation, null);
4324
4325                            if (mucOptions.mamSupport()) {
4326                                getMessageArchiveService().catchupMUC(conversation);
4327                            }
4328                            fetchConferenceMembers(conversation);
4329                            if (mucOptions.isPrivateAndNonAnonymous()) {
4330                                if (followedInvite) {
4331                                    final Bookmark bookmark = conversation.getBookmark();
4332                                    if (bookmark != null) {
4333                                        if (!bookmark.autojoin()) {
4334                                            bookmark.setAutojoin(true);
4335                                            createBookmark(account, bookmark);
4336                                        }
4337                                    } else {
4338                                        saveConversationAsBookmark(conversation, null);
4339                                    }
4340                                }
4341                            }
4342                            synchronized (account.inProgressConferenceJoins) {
4343                                account.inProgressConferenceJoins.remove(conversation);
4344                                sendUnsentMessages(conversation);
4345                            }
4346                        }
4347
4348                        @Override
4349                        public void onConferenceConfigurationFetched(Conversation conversation) {
4350                            if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
4351                                Log.d(
4352                                        Config.LOGTAG,
4353                                        account.getJid().asBareJid()
4354                                                + ": conversation ("
4355                                                + conversation.getJid()
4356                                                + ") got archived before IQ result");
4357                                return;
4358                            }
4359                            join(conversation);
4360                        }
4361
4362                        @Override
4363                        public void onFetchFailed(
4364                                final Conversation conversation, final String errorCondition) {
4365                            if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
4366                                Log.d(
4367                                        Config.LOGTAG,
4368                                        account.getJid().asBareJid()
4369                                                + ": conversation ("
4370                                                + conversation.getJid()
4371                                                + ") got archived before IQ result");
4372                                return;
4373                            }
4374                            if ("remote-server-not-found".equals(errorCondition)) {
4375                                synchronized (account.inProgressConferenceJoins) {
4376                                    account.inProgressConferenceJoins.remove(conversation);
4377                                }
4378                                conversation
4379                                        .getMucOptions()
4380                                        .setError(MucOptions.Error.SERVER_NOT_FOUND);
4381                                updateConversationUi();
4382                            } else {
4383                                join(conversation);
4384                                fetchConferenceConfiguration(conversation);
4385                            }
4386                        }
4387                    });
4388            updateConversationUi();
4389        } else {
4390            synchronized (account.pendingConferenceJoins) {
4391                account.pendingConferenceJoins.add(conversation);
4392            }
4393            conversation.resetMucOptions();
4394            conversation.setHasMessagesLeftOnServer(false);
4395            updateConversationUi();
4396        }
4397    }
4398
4399    private void fetchConferenceMembers(final Conversation conversation) {
4400        final Account account = conversation.getAccount();
4401        final AxolotlService axolotlService = account.getAxolotlService();
4402        final var affiliations = new ArrayList<String>();
4403        affiliations.add("outcast");
4404        if (conversation.getMucOptions().isPrivateAndNonAnonymous()) affiliations.addAll(List.of("member", "admin", "owner"));
4405        final Consumer<Iq> callback =
4406                new Consumer<Iq>() {
4407
4408                    private int i = 0;
4409                    private boolean success = true;
4410
4411                    @Override
4412                    public void accept(Iq response) {
4413                        final boolean omemoEnabled =
4414                                conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL;
4415                        Element query = response.query("http://jabber.org/protocol/muc#admin");
4416                        if (response.getType() == Iq.Type.RESULT && query != null) {
4417                            for (Element child : query.getChildren()) {
4418                                if ("item".equals(child.getName())) {
4419                                    MucOptions.User user =
4420                                            AbstractParser.parseItem(conversation, child);
4421                                    user.setOnline(false);
4422                                    if (!user.realJidMatchesAccount()) {
4423                                        boolean isNew =
4424                                                conversation.getMucOptions().updateUser(user);
4425                                        Contact contact = user.getContact();
4426                                        if (omemoEnabled
4427                                                && isNew
4428                                                && user.getRealJid() != null
4429                                                && (contact == null
4430                                                        || !contact.mutualPresenceSubscription())
4431                                                && axolotlService.hasEmptyDeviceList(
4432                                                        user.getRealJid())) {
4433                                            axolotlService.fetchDeviceIds(user.getRealJid());
4434                                        }
4435                                    }
4436                                }
4437                            }
4438                        } else {
4439                            success = false;
4440                            Log.d(
4441                                    Config.LOGTAG,
4442                                    account.getJid().asBareJid()
4443                                            + ": could not request affiliation "
4444                                            + affiliations.get(i)
4445                                            + " in "
4446                                            + conversation.getJid().asBareJid());
4447                        }
4448                        ++i;
4449                        if (i >= affiliations.size()) {
4450                            final var mucOptions = conversation.getMucOptions();
4451                            List<Jid> members = mucOptions.getMembers(true);
4452                            if (success) {
4453                                List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
4454                                boolean changed = false;
4455                                for (ListIterator<Jid> iterator = cryptoTargets.listIterator();
4456                                        iterator.hasNext(); ) {
4457                                    Jid jid = iterator.next();
4458                                    if (!members.contains(jid)
4459                                            && !members.contains(jid.getDomain())) {
4460                                        iterator.remove();
4461                                        Log.d(
4462                                                Config.LOGTAG,
4463                                                account.getJid().asBareJid()
4464                                                        + ": removed "
4465                                                        + jid
4466                                                        + " from crypto targets of "
4467                                                        + conversation.getName());
4468                                        changed = true;
4469                                    }
4470                                }
4471                                if (changed) {
4472                                    conversation.setAcceptedCryptoTargets(cryptoTargets);
4473                                    updateConversation(conversation);
4474                                }
4475                            }
4476                            getAvatarService().clear(mucOptions);
4477                            updateMucRosterUi();
4478                            updateConversationUi();
4479                        }
4480                    }
4481                };
4482        for (String affiliation : affiliations) {
4483            sendIqPacket(
4484                    account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
4485        }
4486        Log.d(
4487                Config.LOGTAG,
4488                account.getJid().asBareJid() + ": fetching members for " + conversation.getName());
4489    }
4490
4491    public void providePasswordForMuc(final Conversation conversation, final String password) {
4492        if (conversation.getMode() == Conversation.MODE_MULTI) {
4493            conversation.getMucOptions().setPassword(password);
4494            if (conversation.getBookmark() != null) {
4495                final Bookmark bookmark = conversation.getBookmark();
4496                bookmark.setAutojoin(true);
4497                createBookmark(conversation.getAccount(), bookmark);
4498            }
4499            updateConversation(conversation);
4500            joinMuc(conversation);
4501        }
4502    }
4503
4504    public void deleteAvatar(final Account account) {
4505        final AtomicBoolean executed = new AtomicBoolean(false);
4506        final Runnable onDeleted =
4507                () -> {
4508                    if (executed.compareAndSet(false, true)) {
4509                        account.setAvatar(null);
4510                        databaseBackend.updateAccount(account);
4511                        getAvatarService().clear(account);
4512                        updateAccountUi();
4513                    }
4514                };
4515        deleteVcardAvatar(account, onDeleted);
4516        deletePepNode(account, Namespace.AVATAR_DATA);
4517        deletePepNode(account, Namespace.AVATAR_METADATA, onDeleted);
4518    }
4519
4520    public void deletePepNode(final Account account, final String node) {
4521        deletePepNode(account, node, null);
4522    }
4523
4524    private void deletePepNode(final Account account, final String node, final Runnable runnable) {
4525        final Iq request = mIqGenerator.deleteNode(node);
4526        sendIqPacket(
4527                account,
4528                request,
4529                (packet) -> {
4530                    if (packet.getType() == Iq.Type.RESULT) {
4531                        Log.d(
4532                                Config.LOGTAG,
4533                                account.getJid().asBareJid()
4534                                        + ": successfully deleted pep node "
4535                                        + node);
4536                        if (runnable != null) {
4537                            runnable.run();
4538                        }
4539                    } else {
4540                        Log.d(
4541                                Config.LOGTAG,
4542                                account.getJid().asBareJid() + ": failed to delete " + packet);
4543                    }
4544                });
4545    }
4546
4547    private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) {
4548        final Iq retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid());
4549        sendIqPacket(
4550                account,
4551                retrieveVcard,
4552                (response) -> {
4553                    if (response.getType() != Iq.Type.RESULT) {
4554                        Log.d(
4555                                Config.LOGTAG,
4556                                account.getJid().asBareJid() + ": no vCard set. nothing to do");
4557                        return;
4558                    }
4559                    final Element vcard = response.findChild("vCard", "vcard-temp");
4560                    if (vcard == null) {
4561                        Log.d(
4562                                Config.LOGTAG,
4563                                account.getJid().asBareJid() + ": no vCard set. nothing to do");
4564                        return;
4565                    }
4566                    Element photo = vcard.findChild("PHOTO");
4567                    if (photo == null) {
4568                        photo = vcard.addChild("PHOTO");
4569                    }
4570                    photo.clearChildren();
4571                    final Iq publication = new Iq(Iq.Type.SET);
4572                    publication.setTo(account.getJid().asBareJid());
4573                    publication.addChild(vcard);
4574                    sendIqPacket(
4575                            account,
4576                            publication,
4577                            (publicationResponse) -> {
4578                                if (publicationResponse.getType() == Iq.Type.RESULT) {
4579                                    Log.d(
4580                                            Config.LOGTAG,
4581                                            account.getJid().asBareJid()
4582                                                    + ": successfully deleted vcard avatar");
4583                                    runnable.run();
4584                                } else {
4585                                    Log.d(
4586                                            Config.LOGTAG,
4587                                            "failed to publish vcard "
4588                                                    + publicationResponse.getErrorCondition());
4589                                }
4590                            });
4591                });
4592    }
4593
4594    private boolean hasEnabledAccounts() {
4595        if (this.accounts == null) {
4596            return false;
4597        }
4598        for (final Account account : this.accounts) {
4599            if (account.isConnectionEnabled()) {
4600                return true;
4601            }
4602        }
4603        return false;
4604    }
4605
4606    public void getAttachments(
4607            final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) {
4608        getAttachments(
4609                conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded);
4610    }
4611
4612    public void getAttachments(
4613            final Account account,
4614            final Jid jid,
4615            final int limit,
4616            final OnMediaLoaded onMediaLoaded) {
4617        getAttachments(account.getUuid(), jid.asBareJid(), limit, onMediaLoaded);
4618    }
4619
4620    public void getAttachments(
4621            final String account,
4622            final Jid jid,
4623            final int limit,
4624            final OnMediaLoaded onMediaLoaded) {
4625        new Thread(
4626                        () ->
4627                                onMediaLoaded.onMediaLoaded(
4628                                        fileBackend.convertToAttachments(
4629                                                databaseBackend.getRelativeFilePaths(
4630                                                        account, jid, limit))))
4631                .start();
4632    }
4633
4634    public void persistSelfNick(final MucOptions.User self, final boolean modified) {
4635        final Conversation conversation = self.getConversation();
4636        final Account account = conversation.getAccount();
4637        final Jid full = self.getFullJid();
4638        if (!full.equals(conversation.getJid())) {
4639            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persisting full jid " + full);
4640            conversation.setContactJid(full);
4641            databaseBackend.updateConversation(conversation);
4642        }
4643
4644        final String nick = self.getNick();
4645        final Bookmark bookmark = conversation.getBookmark();
4646        if (bookmark == null || !modified) {
4647            return;
4648        }
4649        final String defaultNick = MucOptions.defaultNick(account);
4650        if (nick.equals(defaultNick) || nick.equals(bookmark.getNick())) {
4651            return;
4652        }
4653        Log.d(
4654                Config.LOGTAG,
4655                account.getJid().asBareJid()
4656                        + ": persist nick '"
4657                        + full.getResource()
4658                        + "' into bookmark for "
4659                        + conversation.getJid().asBareJid());
4660        bookmark.setNick(nick);
4661        createBookmark(bookmark.getAccount(), bookmark);
4662    }
4663
4664    public void presenceToMuc(final Conversation conversation) {
4665        final MucOptions options = conversation.getMucOptions();
4666        if (options.online()) {
4667            Account account = conversation.getAccount();
4668            final Jid joinJid = options.getSelf().getFullJid();
4669            final var packet = mPresenceGenerator.selfPresence(account, Presence.Availability.ONLINE, options.nonanonymous(), options.getSelf().getNick());
4670            packet.setTo(joinJid);
4671            sendPresencePacket(account, packet);
4672        }
4673    }
4674
4675    public boolean renameInMuc(
4676            final Conversation conversation,
4677            final String nick,
4678            final UiCallback<Conversation> callback) {
4679        final Account account = conversation.getAccount();
4680        final Bookmark bookmark = conversation.getBookmark();
4681        final MucOptions options = conversation.getMucOptions();
4682        final Jid joinJid = options.createJoinJid(nick);
4683        if (joinJid == null) {
4684            return false;
4685        }
4686        if (options.online()) {
4687            maybeRegisterWithMuc(conversation, nick);
4688            options.setOnRenameListener(
4689                    new OnRenameListener() {
4690
4691                        @Override
4692                        public void onSuccess() {
4693                            final var packet = mPresenceGenerator.selfPresence(account, Presence.Availability.ONLINE, options.nonanonymous(), nick);
4694                            packet.setTo(joinJid);
4695                            sendPresencePacket(account, packet);
4696                            callback.success(conversation);
4697                        }
4698
4699                        @Override
4700                        public void onFailure() {
4701                            callback.error(R.string.nick_in_use, conversation);
4702                        }
4703                    });
4704
4705            final var packet =
4706                    mPresenceGenerator.selfPresence(
4707                            account,
4708                            im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
4709                            options.nonanonymous(), nick);
4710            packet.setTo(joinJid);
4711            sendPresencePacket(account, packet);
4712            if (nick.equals(MucOptions.defaultNick(account))
4713                    && bookmark != null
4714                    && bookmark.getNick() != null) {
4715                Log.d(
4716                        Config.LOGTAG,
4717                        account.getJid().asBareJid()
4718                                + ": removing nick from bookmark for "
4719                                + bookmark.getJid());
4720                bookmark.setNick(null);
4721                createBookmark(account, bookmark);
4722            }
4723        } else {
4724            conversation.setContactJid(joinJid);
4725            databaseBackend.updateConversation(conversation);
4726            if (account.getStatus() == Account.State.ONLINE) {
4727                if (bookmark != null) {
4728                    bookmark.setNick(nick);
4729                    createBookmark(account, bookmark);
4730                }
4731                joinMuc(conversation);
4732            }
4733        }
4734        return true;
4735    }
4736
4737    public void checkMucRequiresRename() {
4738        synchronized (this.conversations) {
4739            for (final Conversation conversation : this.conversations) {
4740                if (conversation.getMode() == Conversational.MODE_MULTI) {
4741                    checkMucRequiresRename(conversation);
4742                }
4743            }
4744        }
4745    }
4746
4747    private void checkMucRequiresRename(final Conversation conversation) {
4748        final var options = conversation.getMucOptions();
4749        if (!options.online()) {
4750            return;
4751        }
4752        final var account = conversation.getAccount();
4753        final String current = options.getActualNick();
4754        final String proposed = options.getProposedNickPure();
4755        if (current == null || current.equals(proposed)) {
4756            return;
4757        }
4758        final Jid joinJid = options.createJoinJid(proposed);
4759        Log.d(
4760                Config.LOGTAG,
4761                String.format(
4762                        "%s: muc rename required %s (was: %s)",
4763                        account.getJid().asBareJid(), joinJid, current));
4764        final var packet =
4765                mPresenceGenerator.selfPresence(
4766                        account,
4767                        im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
4768                        options.nonanonymous(), proposed);
4769        packet.setTo(joinJid);
4770        sendPresencePacket(account, packet);
4771    }
4772
4773    public void leaveMuc(Conversation conversation) {
4774        leaveMuc(conversation, false);
4775    }
4776
4777    private void leaveMuc(Conversation conversation, boolean now) {
4778        final Account account = conversation.getAccount();
4779        synchronized (account.pendingConferenceJoins) {
4780            account.pendingConferenceJoins.remove(conversation);
4781        }
4782        synchronized (account.pendingConferenceLeaves) {
4783            account.pendingConferenceLeaves.remove(conversation);
4784        }
4785        if (account.getStatus() == Account.State.ONLINE || now) {
4786            sendPresencePacket(
4787                    conversation.getAccount(),
4788                    mPresenceGenerator.leave(conversation.getMucOptions()));
4789            conversation.getMucOptions().setOffline();
4790            Bookmark bookmark = conversation.getBookmark();
4791            if (bookmark != null) {
4792                bookmark.setConversation(null);
4793            }
4794            Log.d(
4795                    Config.LOGTAG,
4796                    conversation.getAccount().getJid().asBareJid()
4797                            + ": leaving muc "
4798                            + conversation.getJid());
4799            final var connection = account.getXmppConnection();
4800            if (connection != null) {
4801                connection.getManager(DiscoManager.class).clear(conversation.getJid().asBareJid());
4802            }
4803        } else {
4804            synchronized (account.pendingConferenceLeaves) {
4805                account.pendingConferenceLeaves.add(conversation);
4806            }
4807        }
4808    }
4809
4810    public String findConferenceServer(final Account account) {
4811        String server;
4812        if (account.getXmppConnection() != null) {
4813            server = account.getXmppConnection().getMucServer();
4814            if (server != null) {
4815                return server;
4816            }
4817        }
4818        for (Account other : getAccounts()) {
4819            if (other != account && other.getXmppConnection() != null) {
4820                server = other.getXmppConnection().getMucServer();
4821                if (server != null) {
4822                    return server;
4823                }
4824            }
4825        }
4826        return null;
4827    }
4828
4829    public void createPublicChannel(
4830            final Account account,
4831            final String name,
4832            final Jid address,
4833            final UiCallback<Conversation> callback) {
4834        joinMuc(
4835                findOrCreateConversation(account, address, true, false, true),
4836                conversation -> {
4837                    final Bundle configuration = IqGenerator.defaultChannelConfiguration();
4838                    if (!TextUtils.isEmpty(name)) {
4839                        configuration.putString("muc#roomconfig_roomname", name);
4840                    }
4841                    pushConferenceConfiguration(
4842                            conversation,
4843                            configuration,
4844                            new OnConfigurationPushed() {
4845                                @Override
4846                                public void onPushSucceeded() {
4847                                    saveConversationAsBookmark(conversation, name);
4848                                    callback.success(conversation);
4849                                }
4850
4851                                @Override
4852                                public void onPushFailed() {
4853                                    if (conversation
4854                                            .getMucOptions()
4855                                            .getSelf()
4856                                            .getAffiliation()
4857                                            .ranks(MucOptions.Affiliation.OWNER)) {
4858                                        callback.error(
4859                                                R.string.unable_to_set_channel_configuration,
4860                                                conversation);
4861                                    } else {
4862                                        callback.error(
4863                                                R.string.joined_an_existing_channel, conversation);
4864                                    }
4865                                }
4866                            });
4867                });
4868    }
4869
4870    public boolean createAdhocConference(
4871            final Account account,
4872            final String name,
4873            final Iterable<Jid> jids,
4874            final UiCallback<Conversation> callback) {
4875        Log.d(
4876                Config.LOGTAG,
4877                account.getJid().asBareJid().toString()
4878                        + ": creating adhoc conference with "
4879                        + jids.toString());
4880        if (account.getStatus() == Account.State.ONLINE) {
4881            try {
4882                String server = findConferenceServer(account);
4883                if (server == null) {
4884                    if (callback != null) {
4885                        callback.error(R.string.no_conference_server_found, null);
4886                    }
4887                    return false;
4888                }
4889                final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null);
4890                final Conversation conversation =
4891                        findOrCreateConversation(account, jid, true, false, true);
4892                joinMuc(
4893                        conversation,
4894                        new OnConferenceJoined() {
4895                            @Override
4896                            public void onConferenceJoined(final Conversation conversation) {
4897                                final Bundle configuration =
4898                                        IqGenerator.defaultGroupChatConfiguration();
4899                                if (!TextUtils.isEmpty(name)) {
4900                                    configuration.putString("muc#roomconfig_roomname", name);
4901                                }
4902                                pushConferenceConfiguration(
4903                                        conversation,
4904                                        configuration,
4905                                        new OnConfigurationPushed() {
4906                                            @Override
4907                                            public void onPushSucceeded() {
4908                                                for (Jid invite : jids) {
4909                                                    invite(conversation, invite);
4910                                                }
4911                                                for (String resource :
4912                                                        account.getSelfContact()
4913                                                                .getPresences()
4914                                                                .toResourceArray()) {
4915                                                    Jid other =
4916                                                            account.getJid().withResource(resource);
4917                                                    Log.d(
4918                                                            Config.LOGTAG,
4919                                                            account.getJid().asBareJid()
4920                                                                    + ": sending direct invite to "
4921                                                                    + other);
4922                                                    directInvite(conversation, other);
4923                                                }
4924                                                saveConversationAsBookmark(conversation, name);
4925                                                if (callback != null) {
4926                                                    callback.success(conversation);
4927                                                }
4928                                            }
4929
4930                                            @Override
4931                                            public void onPushFailed() {
4932                                                archiveConversation(conversation);
4933                                                if (callback != null) {
4934                                                    callback.error(
4935                                                            R.string.conference_creation_failed,
4936                                                            conversation);
4937                                                }
4938                                            }
4939                                        });
4940                            }
4941                        });
4942                return true;
4943            } catch (IllegalArgumentException e) {
4944                if (callback != null) {
4945                    callback.error(R.string.conference_creation_failed, null);
4946                }
4947                return false;
4948            }
4949        } else {
4950            if (callback != null) {
4951                callback.error(R.string.not_connected_try_again, null);
4952            }
4953            return false;
4954        }
4955    }
4956
4957    public void checkIfMuc(final Account account, final Jid jid, Consumer<Boolean> cb) {
4958        if (jid.isDomainJid()) {
4959            // Spec basically says MUC needs to have a node
4960            // And also specifies that MUC and MUC service should have the same identity...
4961            cb.accept(false);
4962            return;
4963        }
4964
4965        final var connection = account.getXmppConnection();
4966        if (connection == null) {
4967            cb.accept(false); // hmmm...
4968            return;
4969        }
4970        final ListenableFuture<InfoQuery> future =
4971                connection
4972                        .getManager(DiscoManager.class)
4973                        .info(Entity.discoItem(jid), null);
4974
4975        Futures.addCallback(
4976                future,
4977                new FutureCallback<>() {
4978                    @Override
4979                    public void onSuccess(InfoQuery result) {
4980                        cb.accept(
4981                            result.hasFeature("http://jabber.org/protocol/muc") &&
4982                            result.hasIdentityWithCategory("conference")
4983                        );
4984                    }
4985
4986                    @Override
4987                    public void onFailure(@NonNull Throwable throwable) {
4988                        cb.accept(false);
4989                    }
4990                },
4991                MoreExecutors.directExecutor()
4992        );
4993    }
4994
4995    public void fetchConferenceConfiguration(final Conversation conversation) {
4996        fetchConferenceConfiguration(conversation, null);
4997    }
4998
4999    public void fetchConferenceConfiguration(
5000            final Conversation conversation, final OnConferenceConfigurationFetched callback) {
5001        final var account = conversation.getAccount();
5002        final var connection = account.getXmppConnection();
5003        if (connection == null) {
5004            return;
5005        }
5006        final var future =
5007                connection
5008                        .getManager(DiscoManager.class)
5009                        .info(Entity.discoItem(conversation.getJid().asBareJid()), null);
5010        Futures.addCallback(
5011                future,
5012                new FutureCallback<>() {
5013                    @Override
5014                    public void onSuccess(InfoQuery result) {
5015                        final MucOptions mucOptions = conversation.getMucOptions();
5016                        final Bookmark bookmark = conversation.getBookmark();
5017                        final boolean sameBefore =
5018                                StringUtils.equals(
5019                                        bookmark == null ? null : bookmark.getBookmarkName(),
5020                                        mucOptions.getName());
5021
5022                        final var hadOccupantId = mucOptions.occupantId();
5023                        if (mucOptions.updateConfiguration(result)) {
5024                            Log.d(
5025                                    Config.LOGTAG,
5026                                    account.getJid().asBareJid()
5027                                            + ": muc configuration changed for "
5028                                            + conversation.getJid().asBareJid());
5029                            updateConversation(conversation);
5030                        }
5031
5032                        final var hasOccupantId = mucOptions.occupantId();
5033
5034                        if (!hadOccupantId && hasOccupantId && mucOptions.online()) {
5035                            final var me = mucOptions.getSelf().getFullJid();
5036                            Log.d(
5037                                    Config.LOGTAG,
5038                                    account.getJid().asBareJid()
5039                                            + ": gained support for occupant-id in "
5040                                            + me
5041                                            + ". resending presence");
5042                            final var packet =
5043                                    mPresenceGenerator.selfPresence(
5044                                            account,
5045                                            im.conversations.android.xmpp.model.stanza.Presence
5046                                                    .Availability.ONLINE,
5047                                            mucOptions.nonanonymous(), mucOptions.getSelf().getNick());
5048                            packet.setTo(me);
5049                            sendPresencePacket(account, packet);
5050                        }
5051
5052                        if (bookmark != null
5053                                && (sameBefore || bookmark.getBookmarkName() == null)) {
5054                            if (bookmark.setBookmarkName(
5055                                    StringUtils.nullOnEmpty(mucOptions.getName()))) {
5056                                createBookmark(account, bookmark);
5057                            }
5058                        }
5059
5060                        if (callback != null) {
5061                            callback.onConferenceConfigurationFetched(conversation);
5062                        }
5063
5064                        updateConversationUi();
5065                    }
5066
5067                    @Override
5068                    public void onFailure(@NonNull Throwable throwable) {
5069                        if (throwable instanceof TimeoutException) {
5070                            Log.d(
5071                                    Config.LOGTAG,
5072                                    account.getJid().asBareJid()
5073                                            + ": received timeout waiting for conference"
5074                                            + " configuration fetch");
5075                        } else if (throwable
5076                                instanceof IqErrorResponseException errorResponseException) {
5077                            if (callback != null) {
5078                                callback.onFetchFailed(
5079                                        conversation,
5080                                        errorResponseException.getResponse().getErrorCondition());
5081                            }
5082                        }
5083                    }
5084                },
5085                MoreExecutors.directExecutor());
5086    }
5087
5088    public void pushNodeConfiguration(
5089            Account account,
5090            final String node,
5091            final Bundle options,
5092            final OnConfigurationPushed callback) {
5093        pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback);
5094    }
5095
5096    public void pushNodeConfiguration(
5097            Account account,
5098            final Jid jid,
5099            final String node,
5100            final Bundle options,
5101            final OnConfigurationPushed callback) {
5102        Log.d(Config.LOGTAG, "pushing node configuration");
5103        sendIqPacket(
5104                account,
5105                mIqGenerator.requestPubsubConfiguration(jid, node),
5106                responseToRequest -> {
5107                    if (responseToRequest.getType() == Iq.Type.RESULT) {
5108                        Element pubsub =
5109                                responseToRequest.findChild(
5110                                        "pubsub", "http://jabber.org/protocol/pubsub#owner");
5111                        Element configuration =
5112                                pubsub == null ? null : pubsub.findChild("configure");
5113                        Element x =
5114                                configuration == null
5115                                        ? null
5116                                        : configuration.findChild("x", Namespace.DATA);
5117                        if (x != null) {
5118                            final Data data = Data.parse(x);
5119                            data.submit(options);
5120                            sendIqPacket(
5121                                    account,
5122                                    mIqGenerator.publishPubsubConfiguration(jid, node, data),
5123                                    responseToPublish -> {
5124                                        if (responseToPublish.getType() == Iq.Type.RESULT
5125                                                && callback != null) {
5126                                            Log.d(
5127                                                    Config.LOGTAG,
5128                                                    account.getJid().asBareJid()
5129                                                            + ": successfully changed node"
5130                                                            + " configuration for node "
5131                                                            + node);
5132                                            callback.onPushSucceeded();
5133                                        } else if (responseToPublish.getType() == Iq.Type.ERROR
5134                                                && callback != null) {
5135                                            callback.onPushFailed();
5136                                        }
5137                                    });
5138                        } else if (callback != null) {
5139                            callback.onPushFailed();
5140                        }
5141                    } else if (responseToRequest.getType() == Iq.Type.ERROR && callback != null) {
5142                        callback.onPushFailed();
5143                    }
5144                });
5145    }
5146
5147    public void pushConferenceConfiguration(
5148            final Conversation conversation,
5149            final Bundle options,
5150            final OnConfigurationPushed callback) {
5151        if (options.getString("muc#roomconfig_whois", "moderators").equals("anyone")) {
5152            conversation.setAttribute("accept_non_anonymous", true);
5153            updateConversation(conversation);
5154        }
5155        if (options.containsKey("muc#roomconfig_moderatedroom")) {
5156            final boolean moderated = "1".equals(options.getString("muc#roomconfig_moderatedroom"));
5157            options.putString("members_by_default", moderated ? "0" : "1");
5158        }
5159        if (options.containsKey("muc#roomconfig_allowpm")) {
5160            // ejabberd :-/
5161            final boolean allow = "anyone".equals(options.getString("muc#roomconfig_allowpm"));
5162            options.putString("allow_private_messages", allow ? "1" : "0");
5163            options.putString("allow_private_messages_from_visitors", allow ? "anyone" : "nobody");
5164        }
5165        final var account = conversation.getAccount();
5166        final Iq request = new Iq(Iq.Type.GET);
5167        request.setTo(conversation.getJid().asBareJid());
5168        request.query("http://jabber.org/protocol/muc#owner");
5169        sendIqPacket(
5170                account,
5171                request,
5172                response -> {
5173                    if (response.getType() == Iq.Type.RESULT) {
5174                        final Data data =
5175                                Data.parse(response.query().findChild("x", Namespace.DATA));
5176                        data.submit(options);
5177                        final Iq set = new Iq(Iq.Type.SET);
5178                        set.setTo(conversation.getJid().asBareJid());
5179                        set.query("http://jabber.org/protocol/muc#owner").addChild(data);
5180                        sendIqPacket(
5181                                account,
5182                                set,
5183                                packet -> {
5184                                    if (callback != null) {
5185                                        if (packet.getType() == Iq.Type.RESULT) {
5186                                            callback.onPushSucceeded();
5187                                        } else {
5188                                            Log.d(Config.LOGTAG, "failed: " + packet);
5189                                            callback.onPushFailed();
5190                                        }
5191                                    }
5192                                });
5193                    } else {
5194                        if (callback != null) {
5195                            callback.onPushFailed();
5196                        }
5197                    }
5198                });
5199    }
5200
5201    public void pushSubjectToConference(final Conversation conference, final String subject) {
5202        final var packet =
5203                this.getMessageGenerator()
5204                        .conferenceSubject(conference, StringUtils.nullOnEmpty(subject));
5205        this.sendMessagePacket(conference.getAccount(), packet);
5206    }
5207
5208    public void requestVoice(final Account account, final Jid jid) {
5209        final var packet = this.getMessageGenerator().requestVoice(jid);
5210        this.sendMessagePacket(account, packet);
5211    }
5212
5213    public void changeAffiliationInConference(
5214            final Conversation conference,
5215            Jid user,
5216            final MucOptions.Affiliation affiliation,
5217            final OnAffiliationChanged callback) {
5218        final Jid jid = user.asBareJid();
5219        final Iq request =
5220                this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
5221        sendIqPacket(
5222                conference.getAccount(),
5223                request,
5224                (response) -> {
5225                    if (response.getType() == Iq.Type.RESULT) {
5226                        final var mucOptions = conference.getMucOptions();
5227                        mucOptions.changeAffiliation(jid, affiliation);
5228                        getAvatarService().clear(mucOptions);
5229                        if (callback != null) {
5230                            callback.onAffiliationChangedSuccessful(jid);
5231                        } else {
5232                            Log.d(
5233                                    Config.LOGTAG,
5234                                    "changed affiliation of " + user + " to " + affiliation);
5235                        }
5236                    } else if (callback != null) {
5237                        callback.onAffiliationChangeFailed(
5238                                jid, R.string.could_not_change_affiliation);
5239                    } else {
5240                        Log.d(Config.LOGTAG, "unable to change affiliation");
5241                    }
5242                });
5243    }
5244
5245    public void changeRoleInConference(
5246            final Conversation conference, final String nick, MucOptions.Role role) {
5247        final var account = conference.getAccount();
5248        final Iq request = this.mIqGenerator.changeRole(conference, nick, role.toString());
5249        sendIqPacket(
5250                account,
5251                request,
5252                (packet) -> {
5253                    if (packet.getType() != Iq.Type.RESULT) {
5254                        Log.d(
5255                                Config.LOGTAG,
5256                                account.getJid().asBareJid() + " unable to change role of " + nick);
5257                    }
5258                });
5259    }
5260
5261    public void moderateMessage(final Account account, final Message m, final String reason) {
5262        final var request = this.mIqGenerator.moderateMessage(account, m, reason);
5263        sendIqPacket(account, request, (packet) -> {
5264            if (packet.getType() != Iq.Type.RESULT) {
5265                showErrorToastInUi(R.string.unable_to_moderate);
5266                Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to moderate: " + packet);
5267            }
5268        });
5269    }
5270
5271    public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) {
5272        final Iq request = new Iq(Iq.Type.SET);
5273        request.setTo(conversation.getJid().asBareJid());
5274        request.query("http://jabber.org/protocol/muc#owner").addChild("destroy");
5275        sendIqPacket(
5276                conversation.getAccount(),
5277                request,
5278                response -> {
5279                    if (response.getType() == Iq.Type.RESULT) {
5280                        if (callback != null) {
5281                            callback.onRoomDestroySucceeded();
5282                        }
5283                    } else if (response.getType() == Iq.Type.ERROR) {
5284                        if (callback != null) {
5285                            callback.onRoomDestroyFailed();
5286                        }
5287                    }
5288                });
5289    }
5290
5291    private void disconnect(final Account account, boolean force) {
5292        final XmppConnection connection = account.getXmppConnection();
5293        if (connection == null) {
5294            return;
5295        }
5296        if (!force) {
5297            final List<Conversation> conversations = getConversations();
5298            for (Conversation conversation : conversations) {
5299                if (conversation.getAccount() == account) {
5300                    if (conversation.getMode() == Conversation.MODE_MULTI) {
5301                        leaveMuc(conversation, true);
5302                    }
5303                }
5304            }
5305            sendOfflinePresence(account);
5306        }
5307        connection.disconnect(force);
5308    }
5309
5310    @Override
5311    public IBinder onBind(Intent intent) {
5312        return mBinder;
5313    }
5314
5315    public void deleteMessage(Message message) {
5316        mScheduledMessages.remove(message.getUuid());
5317        databaseBackend.deleteMessage(message.getUuid());
5318        ((Conversation) message.getConversation()).remove(message);
5319        updateConversationUi();
5320    }
5321
5322    public void updateMessage(Message message) {
5323        updateMessage(message, true);
5324    }
5325
5326    public void updateMessage(Message message, boolean includeBody) {
5327        databaseBackend.updateMessage(message, includeBody);
5328        updateConversationUi();
5329    }
5330
5331    public void createMessageAsync(final Message message) {
5332        mDatabaseWriterExecutor.execute(() -> databaseBackend.createMessage(message));
5333    }
5334
5335    public void updateMessage(Message message, String uuid) {
5336        if (!databaseBackend.updateMessage(message, uuid)) {
5337            Log.e(Config.LOGTAG, "error updated message in DB after edit");
5338        }
5339        updateConversationUi();
5340    }
5341
5342    public void syncDirtyContacts(Account account) {
5343        for (Contact contact : account.getRoster().getContacts()) {
5344            if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
5345                pushContactToServer(contact);
5346            }
5347            if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
5348                deleteContactOnServer(contact);
5349            }
5350        }
5351    }
5352
5353    protected void unregisterPhoneAccounts(final Account account) {
5354        for (final Contact contact : account.getRoster().getContacts()) {
5355            if (!contact.showInRoster()) {
5356                contact.unregisterAsPhoneAccount(this);
5357            }
5358        }
5359    }
5360
5361    public void createContact(final Contact contact, final boolean autoGrant) {
5362        createContact(contact, autoGrant, null);
5363    }
5364
5365    public void createContact(
5366            final Contact contact, final boolean autoGrant, final String preAuth) {
5367        if (autoGrant) {
5368            contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
5369            contact.setOption(Contact.Options.ASKING);
5370        }
5371        pushContactToServer(contact, preAuth);
5372    }
5373
5374    public void pushContactToServer(final Contact contact) {
5375        pushContactToServer(contact, null);
5376    }
5377
5378    private void pushContactToServer(final Contact contact, final String preAuth) {
5379        contact.resetOption(Contact.Options.DIRTY_DELETE);
5380        contact.setOption(Contact.Options.DIRTY_PUSH);
5381        final Account account = contact.getAccount();
5382        if (account.getStatus() == Account.State.ONLINE) {
5383            final boolean ask = contact.getOption(Contact.Options.ASKING);
5384            final boolean sendUpdates =
5385                    contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
5386                            && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
5387            final Iq iq = new Iq(Iq.Type.SET);
5388            iq.query(Namespace.ROSTER).addChild(contact.asElement());
5389            account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
5390            if (sendUpdates) {
5391                sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
5392            }
5393            if (ask) {
5394                sendPresencePacket(
5395                        account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth));
5396            }
5397        } else {
5398            syncRoster(contact.getAccount());
5399        }
5400    }
5401
5402    public void publishMucAvatar(
5403            final Conversation conversation, final Uri image, final OnAvatarPublication callback) {
5404        new Thread(
5405                        () -> {
5406                            final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
5407                            final int size = Config.AVATAR_SIZE;
5408                            final Avatar avatar =
5409                                    getFileBackend().getPepAvatar(image, size, format);
5410                            if (avatar != null) {
5411                                if (!getFileBackend().save(avatar)) {
5412                                    callback.onAvatarPublicationFailed(
5413                                            R.string.error_saving_avatar);
5414                                    return;
5415                                }
5416                                avatar.owner = conversation.getJid().asBareJid();
5417                                publishMucAvatar(conversation, avatar, callback);
5418                            } else {
5419                                callback.onAvatarPublicationFailed(
5420                                        R.string.error_publish_avatar_converting);
5421                            }
5422                        })
5423                .start();
5424    }
5425
5426    public void publishAvatarAsync(
5427            final Account account,
5428            final Uri image,
5429            final boolean open,
5430            final OnAvatarPublication callback) {
5431        new Thread(() -> publishAvatar(account, image, open, callback)).start();
5432    }
5433
5434    private void publishAvatar(
5435            final Account account,
5436            final Uri image,
5437            final boolean open,
5438            final OnAvatarPublication callback) {
5439        final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
5440        final int size = Config.AVATAR_SIZE;
5441        final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
5442        if (avatar != null) {
5443            if (!getFileBackend().save(avatar)) {
5444                Log.d(Config.LOGTAG, "unable to save vcard");
5445                callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
5446                return;
5447            }
5448            publishAvatar(account, avatar, open, callback);
5449        } else {
5450            callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
5451        }
5452    }
5453
5454    private void publishMucAvatar(
5455            Conversation conversation, Avatar avatar, OnAvatarPublication callback) {
5456        final var account = conversation.getAccount();
5457        final Iq retrieve = mIqGenerator.retrieveVcardAvatar(avatar);
5458        sendIqPacket(
5459                account,
5460                retrieve,
5461                (response) -> {
5462                    boolean itemNotFound =
5463                            response.getType() == Iq.Type.ERROR
5464                                    && response.hasChild("error")
5465                                    && response.findChild("error").hasChild("item-not-found");
5466                    if (response.getType() == Iq.Type.RESULT || itemNotFound) {
5467                        Element vcard = response.findChild("vCard", "vcard-temp");
5468                        if (vcard == null) {
5469                            vcard = new Element("vCard", "vcard-temp");
5470                        }
5471                        Element photo = vcard.findChild("PHOTO");
5472                        if (photo == null) {
5473                            photo = vcard.addChild("PHOTO");
5474                        }
5475                        photo.clearChildren();
5476                        photo.addChild("TYPE").setContent(avatar.type);
5477                        photo.addChild("BINVAL").setContent(avatar.image);
5478                        final Iq publication = new Iq(Iq.Type.SET);
5479                        publication.setTo(conversation.getJid().asBareJid());
5480                        publication.addChild(vcard);
5481                        sendIqPacket(
5482                                account,
5483                                publication,
5484                                (publicationResponse) -> {
5485                                    if (publicationResponse.getType() == Iq.Type.RESULT) {
5486                                        callback.onAvatarPublicationSucceeded();
5487                                    } else {
5488                                        Log.d(
5489                                                Config.LOGTAG,
5490                                                "failed to publish vcard "
5491                                                        + publicationResponse.getErrorCondition());
5492                                        callback.onAvatarPublicationFailed(
5493                                                R.string.error_publish_avatar_server_reject);
5494                                    }
5495                                });
5496                    } else {
5497                        Log.d(Config.LOGTAG, "failed to request vcard " + response);
5498                        callback.onAvatarPublicationFailed(
5499                                R.string.error_publish_avatar_no_server_support);
5500                    }
5501                });
5502    }
5503
5504    public void publishAvatar(
5505            final Account account,
5506            final Avatar avatar,
5507            final boolean open,
5508            final OnAvatarPublication callback) {
5509        final Bundle options;
5510        if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
5511            options = open ? PublishOptions.openAccess() : PublishOptions.presenceAccess();
5512        } else {
5513            options = null;
5514        }
5515        publishAvatar(account, avatar, options, true, callback);
5516    }
5517
5518    public void publishAvatar(
5519            Account account,
5520            final Avatar avatar,
5521            final Bundle options,
5522            final boolean retry,
5523            final OnAvatarPublication callback) {
5524        Log.d(
5525                Config.LOGTAG,
5526                account.getJid().asBareJid() + ": publishing avatar. options=" + options);
5527        final Iq packet = this.mIqGenerator.publishAvatar(avatar, options);
5528        this.sendIqPacket(
5529                account,
5530                packet,
5531                result -> {
5532                    if (result.getType() == Iq.Type.RESULT) {
5533                        publishAvatarMetadata(account, avatar, options, true, callback);
5534                    } else if (retry && PublishOptions.preconditionNotMet(result)) {
5535                        pushNodeConfiguration(
5536                                account,
5537                                Namespace.AVATAR_DATA,
5538                                options,
5539                                new OnConfigurationPushed() {
5540                                    @Override
5541                                    public void onPushSucceeded() {
5542                                        Log.d(
5543                                                Config.LOGTAG,
5544                                                account.getJid().asBareJid()
5545                                                        + ": changed node configuration for avatar"
5546                                                        + " node");
5547                                        publishAvatar(account, avatar, options, false, callback);
5548                                    }
5549
5550                                    @Override
5551                                    public void onPushFailed() {
5552                                        Log.d(
5553                                                Config.LOGTAG,
5554                                                account.getJid().asBareJid()
5555                                                        + ": unable to change node configuration"
5556                                                        + " for avatar node");
5557                                        publishAvatar(account, avatar, null, false, callback);
5558                                    }
5559                                });
5560                    } else {
5561                        Element error = result.findChild("error");
5562                        Log.d(
5563                                Config.LOGTAG,
5564                                account.getJid().asBareJid()
5565                                        + ": server rejected avatar "
5566                                        + (avatar.size / 1024)
5567                                        + "KiB "
5568                                        + (error != null ? error.toString() : ""));
5569                        if (callback != null) {
5570                            callback.onAvatarPublicationFailed(
5571                                    R.string.error_publish_avatar_server_reject);
5572                        }
5573                    }
5574                });
5575    }
5576
5577    public void publishAvatarMetadata(
5578            Account account,
5579            final Avatar avatar,
5580            final Bundle options,
5581            final boolean retry,
5582            final OnAvatarPublication callback) {
5583        final Iq packet =
5584                XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options);
5585        sendIqPacket(
5586                account,
5587                packet,
5588                result -> {
5589                    if (result.getType() == Iq.Type.RESULT) {
5590                        if (account.setAvatar(avatar.getFilename())) {
5591                            getAvatarService().clear(account);
5592                            databaseBackend.updateAccount(account);
5593                            notifyAccountAvatarHasChanged(account);
5594                        }
5595                        Log.d(
5596                                Config.LOGTAG,
5597                                account.getJid().asBareJid()
5598                                        + ": published avatar "
5599                                        + (avatar.size / 1024)
5600                                        + "KiB");
5601                        if (callback != null) {
5602                            callback.onAvatarPublicationSucceeded();
5603                        }
5604                    } else if (retry && PublishOptions.preconditionNotMet(result)) {
5605                        pushNodeConfiguration(
5606                                account,
5607                                Namespace.AVATAR_METADATA,
5608                                options,
5609                                new OnConfigurationPushed() {
5610                                    @Override
5611                                    public void onPushSucceeded() {
5612                                        Log.d(
5613                                                Config.LOGTAG,
5614                                                account.getJid().asBareJid()
5615                                                        + ": changed node configuration for avatar"
5616                                                        + " meta data node");
5617                                        publishAvatarMetadata(
5618                                                account, avatar, options, false, callback);
5619                                    }
5620
5621                                    @Override
5622                                    public void onPushFailed() {
5623                                        Log.d(
5624                                                Config.LOGTAG,
5625                                                account.getJid().asBareJid()
5626                                                        + ": unable to change node configuration"
5627                                                        + " for avatar meta data node");
5628                                        publishAvatarMetadata(
5629                                                account, avatar, null, false, callback);
5630                                    }
5631                                });
5632                    } else {
5633                        if (callback != null) {
5634                            callback.onAvatarPublicationFailed(
5635                                    R.string.error_publish_avatar_server_reject);
5636                        }
5637                    }
5638                });
5639    }
5640
5641    public void republishAvatarIfNeeded(final Account account) {
5642        if (account.getAxolotlService().isPepBroken()) {
5643            Log.d(
5644                    Config.LOGTAG,
5645                    account.getJid().asBareJid()
5646                            + ": skipping republication of avatar because pep is broken");
5647            return;
5648        }
5649        final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
5650        this.sendIqPacket(
5651                account,
5652                packet,
5653                new Consumer<Iq>() {
5654
5655                    private Avatar parseAvatar(final Iq packet) {
5656                        final var pubsub = packet.getExtension(PubSub.class);
5657                        if (pubsub == null) {
5658                            return null;
5659                        }
5660                        final var items = pubsub.getItems();
5661                        if (items == null) {
5662                            return null;
5663                        }
5664                        final var item = items.getFirstItemWithId(Metadata.class);
5665                        if (item == null) {
5666                            return null;
5667                        }
5668                        return Avatar.parseMetadata(item.getKey(), item.getValue());
5669                    }
5670
5671                    private boolean errorIsItemNotFound(Iq packet) {
5672                        Element error = packet.findChild("error");
5673                        return packet.getType() == Iq.Type.ERROR
5674                                && error != null
5675                                && error.hasChild("item-not-found");
5676                    }
5677
5678                    @Override
5679                    public void accept(final Iq packet) {
5680                        if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) {
5681                            final Avatar serverAvatar = parseAvatar(packet);
5682                            if (serverAvatar == null && account.getAvatar() != null) {
5683                                final Avatar avatar =
5684                                        fileBackend.getStoredPepAvatar(account.getAvatar());
5685                                if (avatar != null) {
5686                                    Log.d(
5687                                            Config.LOGTAG,
5688                                            account.getJid().asBareJid()
5689                                                    + ": avatar on server was null. republishing");
5690                                    // publishing as 'open' - old server (that requires
5691                                    // republication) likely doesn't support access models anyway
5692                                    publishAvatar(
5693                                            account,
5694                                            fileBackend.getStoredPepAvatar(account.getAvatar()),
5695                                            true,
5696                                            null);
5697                                } else {
5698                                    Log.e(
5699                                            Config.LOGTAG,
5700                                            account.getJid().asBareJid()
5701                                                    + ": error rereading avatar");
5702                                }
5703                            }
5704                        }
5705                    }
5706                });
5707    }
5708
5709    public void cancelAvatarFetches(final Account account) {
5710        synchronized (mInProgressAvatarFetches) {
5711            for (final Iterator<String> iterator = mInProgressAvatarFetches.iterator();
5712                    iterator.hasNext(); ) {
5713                final String KEY = iterator.next();
5714                if (KEY.startsWith(account.getJid().asBareJid() + "_")) {
5715                    iterator.remove();
5716                }
5717            }
5718        }
5719    }
5720
5721    public void fetchAvatar(Account account, Avatar avatar) {
5722        fetchAvatar(account, avatar, null);
5723    }
5724
5725    public void fetchAvatar(
5726            Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5727        if (databaseBackend.isBlockedMedia(avatar.cid())) {
5728            if (callback != null) callback.error(0, null);
5729            return;
5730        }
5731
5732        final String KEY = generateFetchKey(account, avatar);
5733        synchronized (this.mInProgressAvatarFetches) {
5734            if (mInProgressAvatarFetches.add(KEY)) {
5735                switch (avatar.origin) {
5736                    case PEP:
5737                        this.mInProgressAvatarFetches.add(KEY);
5738                        fetchAvatarPep(account, avatar, callback);
5739                        break;
5740                    case VCARD:
5741                        this.mInProgressAvatarFetches.add(KEY);
5742                        fetchAvatarVcard(account, avatar, callback);
5743                        break;
5744                }
5745            } else if (avatar.origin == Avatar.Origin.PEP) {
5746                mOmittedPepAvatarFetches.add(KEY);
5747            } else {
5748                Log.d(
5749                        Config.LOGTAG,
5750                        account.getJid().asBareJid()
5751                                + ": already fetching "
5752                                + avatar.origin
5753                                + " avatar for "
5754                                + avatar.owner);
5755            }
5756        }
5757    }
5758
5759    private void fetchAvatarPep(
5760            final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5761        final Iq packet = this.mIqGenerator.retrievePepAvatar(avatar);
5762        sendIqPacket(
5763                account,
5764                packet,
5765                (result) -> {
5766                    synchronized (mInProgressAvatarFetches) {
5767                        mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
5768                    }
5769                    final String ERROR =
5770                            account.getJid().asBareJid()
5771                                    + ": fetching avatar for "
5772                                    + avatar.owner
5773                                    + " failed ";
5774                    if (result.getType() == Iq.Type.RESULT) {
5775                        avatar.image = IqParser.avatarData(result);
5776                        if (avatar.image != null) {
5777                            if (getFileBackend().save(avatar)) {
5778                                if (account.getJid().asBareJid().equals(avatar.owner)) {
5779                                    if (account.setAvatar(avatar.getFilename())) {
5780                                        databaseBackend.updateAccount(account);
5781                                    }
5782                                    getAvatarService().clear(account);
5783                                    updateConversationUi();
5784                                    updateAccountUi();
5785                                } else {
5786                                    final Contact contact =
5787                                            account.getRoster().getContact(avatar.owner);
5788                                    contact.setAvatar(avatar);
5789                                    syncRoster(account);
5790                                    getAvatarService().clear(contact);
5791                                    updateConversationUi();
5792                                    updateRosterUi(UpdateRosterReason.AVATAR);
5793                                }
5794                                if (callback != null) {
5795                                    callback.success(avatar);
5796                                }
5797                                Log.d(
5798                                        Config.LOGTAG,
5799                                        account.getJid().asBareJid()
5800                                                + ": successfully fetched pep avatar for "
5801                                                + avatar.owner);
5802                                return;
5803                            }
5804                        } else {
5805
5806                            Log.d(Config.LOGTAG, ERROR + "(parsing error)");
5807                        }
5808                    } else {
5809                        Element error = result.findChild("error");
5810                        if (error == null) {
5811                            Log.d(Config.LOGTAG, ERROR + "(server error)");
5812                        } else {
5813                            Log.d(Config.LOGTAG, ERROR + error);
5814                        }
5815                    }
5816                    if (callback != null) {
5817                        callback.error(0, null);
5818                    }
5819                });
5820    }
5821
5822    private void fetchAvatarVcard(
5823            final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5824        final Iq packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
5825        this.sendIqPacket(
5826                account,
5827                packet,
5828                response -> {
5829                    final boolean previouslyOmittedPepFetch;
5830                    synchronized (mInProgressAvatarFetches) {
5831                        final String KEY = generateFetchKey(account, avatar);
5832                        mInProgressAvatarFetches.remove(KEY);
5833                        previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY);
5834                    }
5835                    if (response.getType() == Iq.Type.RESULT) {
5836                        Element vCard = response.findChild("vCard", "vcard-temp");
5837                        Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
5838                        String image = photo != null ? photo.findChildContent("BINVAL") : null;
5839                        if (image != null) {
5840                            avatar.image = image;
5841                            if (getFileBackend().save(avatar)) {
5842                                Log.d(
5843                                        Config.LOGTAG,
5844                                        account.getJid().asBareJid()
5845                                                + ": successfully fetched vCard avatar for "
5846                                                + avatar.owner
5847                                                + " omittedPep="
5848                                                + previouslyOmittedPepFetch);
5849                                if (avatar.owner.isBareJid()) {
5850                                    if (account.getJid().asBareJid().equals(avatar.owner)
5851                                            && account.getAvatar() == null) {
5852                                        Log.d(
5853                                                Config.LOGTAG,
5854                                                account.getJid().asBareJid()
5855                                                        + ": had no avatar. replacing with vcard");
5856                                        account.setAvatar(avatar.getFilename());
5857                                        databaseBackend.updateAccount(account);
5858                                        getAvatarService().clear(account);
5859                                        updateAccountUi();
5860                                    } else {
5861                                        final Contact contact =
5862                                                account.getRoster().getContact(avatar.owner);
5863                                        contact.setAvatar(avatar, previouslyOmittedPepFetch);
5864                                        syncRoster(account);
5865                                        getAvatarService().clear(contact);
5866                                        updateRosterUi(UpdateRosterReason.AVATAR);
5867                                    }
5868                                    updateConversationUi();
5869                                } else {
5870                                    Conversation conversation =
5871                                            find(account, avatar.owner.asBareJid());
5872                                    if (conversation != null
5873                                            && conversation.getMode() == Conversation.MODE_MULTI) {
5874                                        MucOptions.User user =
5875                                                conversation
5876                                                        .getMucOptions()
5877                                                        .findUserByFullJid(avatar.owner);
5878                                        if (user != null) {
5879                                            if (user.setAvatar(avatar)) {
5880                                                getAvatarService().clear(user);
5881                                                updateConversationUi();
5882                                                updateMucRosterUi();
5883                                            }
5884                                            if (user.getRealJid() != null) {
5885                                                Contact contact =
5886                                                        account.getRoster()
5887                                                                .getContact(user.getRealJid());
5888                                                contact.setAvatar(avatar);
5889                                                syncRoster(account);
5890                                                getAvatarService().clear(contact);
5891                                                updateRosterUi(UpdateRosterReason.AVATAR);
5892                                            }
5893                                        }
5894                                    }
5895                                }
5896                            }
5897                        }
5898                    }
5899                });
5900    }
5901
5902    public void checkForAvatar(final Account account, final UiCallback<Avatar> callback) {
5903        final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
5904        this.sendIqPacket(
5905                account,
5906                packet,
5907                response -> {
5908                    if (response.getType() != Iq.Type.RESULT) {
5909                        callback.error(0, null);
5910                    }
5911                    final var pubsub = packet.getExtension(PubSub.class);
5912                    if (pubsub == null) {
5913                        callback.error(0, null);
5914                        return;
5915                    }
5916                    final var items = pubsub.getItems();
5917                    if (items == null) {
5918                        callback.error(0, null);
5919                        return;
5920                    }
5921                    final var item = items.getFirstItemWithId(Metadata.class);
5922                    if (item == null) {
5923                        callback.error(0, null);
5924                        return;
5925                    }
5926                    final var avatar = Avatar.parseMetadata(item.getKey(), item.getValue());
5927                    if (avatar == null) {
5928                        callback.error(0, null);
5929                        return;
5930                    }
5931                    avatar.owner = account.getJid().asBareJid();
5932                    if (fileBackend.isAvatarCached(avatar)) {
5933                        if (account.setAvatar(avatar.getFilename())) {
5934                            databaseBackend.updateAccount(account);
5935                        }
5936                        getAvatarService().clear(account);
5937                        callback.success(avatar);
5938                    } else {
5939                        fetchAvatarPep(account, avatar, callback);
5940                    }
5941                });
5942    }
5943
5944    public void notifyAccountAvatarHasChanged(final Account account) {
5945        final XmppConnection connection = account.getXmppConnection();
5946        if (connection != null && connection.getFeatures().bookmarksConversion()) {
5947            Log.d(
5948                    Config.LOGTAG,
5949                    account.getJid().asBareJid()
5950                            + ": avatar changed. resending presence to online group chats");
5951            for (Conversation conversation : conversations) {
5952                if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) {
5953                    presenceToMuc(conversation);
5954                }
5955            }
5956        }
5957    }
5958
5959    public void fetchVcard4(Account account, final Contact contact, final Consumer<Element> callback) {
5960        final var packet = this.mIqGenerator.retrieveVcard4(contact.getJid());
5961        sendIqPacket(account, packet, (result) -> {
5962            if (result.getType() == Iq.Type.RESULT) {
5963                final Element item = IqParser.getItem(result);
5964                if (item != null) {
5965                    final Element vcard4 = item.findChild("vcard", Namespace.VCARD4);
5966                    if (vcard4 != null) {
5967                        if (callback != null) {
5968                            callback.accept(vcard4);
5969                        }
5970                        return;
5971                    }
5972                }
5973            } else {
5974                Element error = result.findChild("error");
5975                if (error == null) {
5976                    Log.d(Config.LOGTAG, "fetchVcard4 (server error)");
5977                } else {
5978                    Log.d(Config.LOGTAG, "fetchVcard4 " + error.toString());
5979                }
5980            }
5981            if (callback != null) {
5982                callback.accept(null);
5983            }
5984
5985        });
5986    }
5987
5988    public void deleteContactOnServer(Contact contact) {
5989        contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
5990        contact.resetOption(Contact.Options.DIRTY_PUSH);
5991        contact.setOption(Contact.Options.DIRTY_DELETE);
5992        Account account = contact.getAccount();
5993        if (account.getStatus() == Account.State.ONLINE) {
5994            final Iq iq = new Iq(Iq.Type.SET);
5995            Element item = iq.query(Namespace.ROSTER).addChild("item");
5996            item.setAttribute("jid", contact.getJid());
5997            item.setAttribute("subscription", "remove");
5998            account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
5999        }
6000    }
6001
6002    public void updateConversation(final Conversation conversation) {
6003        mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation));
6004    }
6005
6006    private void reconnectAccount(
6007            final Account account, final boolean force, final boolean interactive) {
6008        synchronized (account) {
6009            final XmppConnection existingConnection = account.getXmppConnection();
6010            final XmppConnection connection;
6011            if (existingConnection != null) {
6012                connection = existingConnection;
6013            } else if (account.isConnectionEnabled()) {
6014                connection = createConnection(account);
6015                account.setXmppConnection(connection);
6016            } else {
6017                return;
6018            }
6019            final boolean hasInternet = hasInternetConnection();
6020            if (account.isConnectionEnabled() && hasInternet) {
6021                if (!force) {
6022                    disconnect(account, false);
6023                }
6024                Thread thread = new Thread(connection);
6025                connection.setInteractive(interactive);
6026                connection.prepareNewConnection();
6027                connection.interrupt();
6028                thread.start();
6029                scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
6030            } else {
6031                disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
6032                account.getRoster().clearPresences();
6033                connection.resetEverything();
6034                final AxolotlService axolotlService = account.getAxolotlService();
6035                if (axolotlService != null) {
6036                    axolotlService.resetBrokenness();
6037                }
6038                if (!hasInternet) {
6039                    account.setStatus(Account.State.NO_INTERNET);
6040                }
6041            }
6042        }
6043    }
6044
6045    public void reconnectAccountInBackground(final Account account) {
6046        new Thread(() -> reconnectAccount(account, false, true)).start();
6047    }
6048
6049    public void invite(final Conversation conversation, final Jid contact) {
6050        Log.d(
6051                Config.LOGTAG,
6052                conversation.getAccount().getJid().asBareJid()
6053                        + ": inviting "
6054                        + contact
6055                        + " to "
6056                        + conversation.getJid().asBareJid());
6057        final MucOptions.User user =
6058                conversation.getMucOptions().findUserByRealJid(contact.asBareJid());
6059        if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) {
6060            changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null);
6061        }
6062        final var packet = mMessageGenerator.invite(conversation, contact);
6063        sendMessagePacket(conversation.getAccount(), packet);
6064    }
6065
6066    public void directInvite(Conversation conversation, Jid jid) {
6067        final var packet = mMessageGenerator.directInvite(conversation, jid);
6068        sendMessagePacket(conversation.getAccount(), packet);
6069    }
6070
6071    public void resetSendingToWaiting(Account account) {
6072        for (Conversation conversation : getConversations()) {
6073            if (conversation.getAccount() == account) {
6074                conversation.findUnsentTextMessages(
6075                        message -> markMessage(message, Message.STATUS_WAITING));
6076            }
6077        }
6078    }
6079
6080    public Message markMessage(
6081            final Account account, final Jid recipient, final String uuid, final int status) {
6082        return markMessage(account, recipient, uuid, status, null);
6083    }
6084
6085    public Message markMessage(
6086            final Account account,
6087            final Jid recipient,
6088            final String uuid,
6089            final int status,
6090            String errorMessage) {
6091        if (uuid == null) {
6092            return null;
6093        }
6094        for (Conversation conversation : getConversations()) {
6095            if (conversation.getJid().asBareJid().equals(recipient)
6096                    && conversation.getAccount() == account) {
6097                final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
6098                if (message != null) {
6099                    markMessage(message, status, errorMessage);
6100                }
6101                return message;
6102            }
6103        }
6104        return null;
6105    }
6106
6107    public boolean markMessage(
6108            final Conversation conversation,
6109            final String uuid,
6110            final int status,
6111            final String serverMessageId) {
6112        return markMessage(conversation, uuid, status, serverMessageId, null, null, null, null, null);
6113    }
6114
6115    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) {
6116        if (uuid == null) {
6117            return false;
6118        } else {
6119            final Message message = conversation.findSentMessageWithUuid(uuid);
6120            if (message != null) {
6121                if (message.getServerMsgId() == null) {
6122                    message.setServerMsgId(serverMessageId);
6123                }
6124                if (message.getEncryption() == Message.ENCRYPTION_NONE && (body != null || html != null || subject != null || thread != null || attachments != null)) {
6125                    message.setBody(body.content);
6126                    if (body.count > 1) {
6127                        message.setBodyLanguage(body.language);
6128                    }
6129                    message.setHtml(html);
6130                    message.setSubject(subject);
6131                    message.setThread(thread);
6132                    if (attachments != null && attachments.isEmpty()) {
6133                        message.setRelativeFilePath(null);
6134                        message.resetFileParams();
6135                    }
6136                    markMessage(message, status, null, true);
6137                } else {
6138                    markMessage(message, status);
6139                }
6140                return true;
6141            } else {
6142                return false;
6143            }
6144        }
6145    }
6146
6147    public void markMessage(Message message, int status) {
6148        markMessage(message, status, null);
6149    }
6150
6151    public void markMessage(final Message message, final int status, final String errorMessage) {
6152        markMessage(message, status, errorMessage, false);
6153    }
6154
6155    public void markMessage(
6156            final Message message,
6157            final int status,
6158            final String errorMessage,
6159            final boolean includeBody) {
6160        final int oldStatus = message.getStatus();
6161        if (status == Message.STATUS_SEND_FAILED
6162                && (oldStatus == Message.STATUS_SEND_RECEIVED
6163                        || oldStatus == Message.STATUS_SEND_DISPLAYED)) {
6164            return;
6165        }
6166        if (status == Message.STATUS_SEND_RECEIVED && oldStatus == Message.STATUS_SEND_DISPLAYED) {
6167            return;
6168        }
6169        message.setErrorMessage(errorMessage);
6170        message.setStatus(status);
6171        databaseBackend.updateMessage(message, includeBody);
6172        updateConversationUi();
6173        if (oldStatus != status && status == Message.STATUS_SEND_FAILED) {
6174            mNotificationService.pushFailedDelivery(message);
6175        }
6176    }
6177
6178    public SharedPreferences getPreferences() {
6179        return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
6180    }
6181
6182    public long getAutomaticMessageDeletionDate() {
6183        final long timeout =
6184                getLongPreference(
6185                        AppSettings.AUTOMATIC_MESSAGE_DELETION,
6186                        R.integer.automatic_message_deletion);
6187        return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000));
6188    }
6189
6190    public long getLongPreference(String name, @IntegerRes int res) {
6191        long defaultValue = getResources().getInteger(res);
6192        try {
6193            return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue)));
6194        } catch (NumberFormatException e) {
6195            return defaultValue;
6196        }
6197    }
6198
6199    public boolean getBooleanPreference(String name, @BoolRes int res) {
6200        return getPreferences().getBoolean(name, getResources().getBoolean(res));
6201    }
6202
6203    public String getStringPreference(String name, @BoolRes int res) {
6204        return getPreferences().getString(name, getResources().getString(res));
6205    }
6206
6207    public boolean confirmMessages() {
6208        return appSettings.isConfirmMessages();
6209    }
6210
6211    public boolean allowMessageCorrection() {
6212        return appSettings.isAllowMessageCorrection();
6213    }
6214
6215    public boolean sendChatStates() {
6216        return getBooleanPreference("chat_states", R.bool.chat_states);
6217    }
6218
6219    public boolean useTorToConnect() {
6220        return appSettings.isUseTor();
6221    }
6222
6223    public boolean broadcastLastActivity() {
6224        return appSettings.isBroadcastLastActivity();
6225    }
6226
6227    public int unreadCount() {
6228        int count = 0;
6229        for (Conversation conversation : getConversations()) {
6230            count += conversation.unreadCount(this);
6231        }
6232        return count;
6233    }
6234
6235    private <T> List<T> threadSafeList(Set<T> set) {
6236        synchronized (LISTENER_LOCK) {
6237            return set.isEmpty() ? Collections.emptyList() : new ArrayList<>(set);
6238        }
6239    }
6240
6241    public void showErrorToastInUi(int resId) {
6242        for (OnShowErrorToast listener : threadSafeList(this.mOnShowErrorToasts)) {
6243            listener.onShowErrorToast(resId);
6244        }
6245    }
6246
6247    public void updateConversationUi() {
6248        updateConversationUi(false);
6249    }
6250
6251    public void updateConversationUi(boolean newCaps) {
6252        for (OnConversationUpdate listener : threadSafeList(this.mOnConversationUpdates)) {
6253            listener.onConversationUpdate(newCaps);
6254        }
6255    }
6256
6257    public void notifyJingleRtpConnectionUpdate(
6258            final Account account,
6259            final Jid with,
6260            final String sessionId,
6261            final RtpEndUserState state) {
6262        for (OnJingleRtpConnectionUpdate listener :
6263                threadSafeList(this.onJingleRtpConnectionUpdate)) {
6264            listener.onJingleRtpConnectionUpdate(account, with, sessionId, state);
6265        }
6266    }
6267
6268    public void notifyJingleRtpConnectionUpdate(
6269            CallIntegration.AudioDevice selectedAudioDevice,
6270            Set<CallIntegration.AudioDevice> availableAudioDevices) {
6271        for (OnJingleRtpConnectionUpdate listener :
6272                threadSafeList(this.onJingleRtpConnectionUpdate)) {
6273            listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
6274        }
6275    }
6276
6277    public void updateAccountUi() {
6278        for (final OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) {
6279            listener.onAccountUpdate();
6280        }
6281    }
6282
6283    public void updateRosterUi(final UpdateRosterReason reason) {
6284        if (reason == UpdateRosterReason.PRESENCE) throw new IllegalArgumentException("PRESENCE must also come with a contact");
6285        updateRosterUi(reason, null);
6286    }
6287
6288    public void updateRosterUi(final UpdateRosterReason reason, final Contact contact) {
6289        for (OnRosterUpdate listener : threadSafeList(this.mOnRosterUpdates)) {
6290            listener.onRosterUpdate(reason, contact);
6291        }
6292    }
6293
6294    public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
6295        if (mOnCaptchaRequested.size() > 0) {
6296            DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
6297            Bitmap scaled =
6298                    Bitmap.createScaledBitmap(
6299                            captcha,
6300                            (int) (captcha.getWidth() * metrics.scaledDensity),
6301                            (int) (captcha.getHeight() * metrics.scaledDensity),
6302                            false);
6303            for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
6304                listener.onCaptchaRequested(account, id, data, scaled);
6305            }
6306            return true;
6307        }
6308        return false;
6309    }
6310
6311    public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
6312        for (OnUpdateBlocklist listener : threadSafeList(this.mOnUpdateBlocklist)) {
6313            listener.OnUpdateBlocklist(status);
6314        }
6315    }
6316
6317    public void updateMucRosterUi() {
6318        for (OnMucRosterUpdate listener : threadSafeList(this.mOnMucRosterUpdate)) {
6319            listener.onMucRosterUpdate();
6320        }
6321    }
6322
6323    public void keyStatusUpdated(AxolotlService.FetchStatus report) {
6324        for (OnKeyStatusUpdated listener : threadSafeList(this.mOnKeyStatusUpdated)) {
6325            listener.onKeyStatusUpdated(report);
6326        }
6327    }
6328
6329    public Account findAccountByJid(final Jid jid) {
6330        for (final Account account : this.accounts) {
6331            if (account.getJid().asBareJid().equals(jid.asBareJid())) {
6332                return account;
6333            }
6334        }
6335        return null;
6336    }
6337
6338    public Account findAccountByUuid(final String uuid) {
6339        for (Account account : this.accounts) {
6340            if (account.getUuid().equals(uuid)) {
6341                return account;
6342            }
6343        }
6344        return null;
6345    }
6346
6347    public Conversation findConversationByUuid(String uuid) {
6348        for (Conversation conversation : getConversations()) {
6349            if (conversation.getUuid().equals(uuid)) {
6350                return conversation;
6351            }
6352        }
6353        return null;
6354    }
6355
6356    public Conversation findUniqueConversationByJid(XmppUri xmppUri) {
6357        List<Conversation> findings = new ArrayList<>();
6358        for (Conversation c : getConversations()) {
6359            if (c.getAccount().isEnabled()
6360                    && c.getJid().asBareJid().equals(xmppUri.getJid().asBareJid())
6361                    && ((c.getMode() == Conversational.MODE_MULTI)
6362                            == xmppUri.isAction(XmppUri.ACTION_JOIN))) {
6363                findings.add(c);
6364            }
6365        }
6366        return findings.size() == 1 ? findings.get(0) : null;
6367    }
6368
6369    public boolean markRead(final Conversation conversation, boolean dismiss) {
6370        return markRead(conversation, null, dismiss).size() > 0;
6371    }
6372
6373    public void markRead(final Conversation conversation) {
6374        markRead(conversation, null, true);
6375    }
6376
6377    public List<Message> markRead(
6378            final Conversation conversation, String upToUuid, boolean dismiss) {
6379        if (dismiss) {
6380            mNotificationService.clear(conversation);
6381        }
6382        final List<Message> readMessages = conversation.markRead(upToUuid);
6383        if (readMessages.size() > 0) {
6384            Runnable runnable =
6385                    () -> {
6386                        for (Message message : readMessages) {
6387                            databaseBackend.updateMessage(message, false);
6388                        }
6389                    };
6390            mDatabaseWriterExecutor.execute(runnable);
6391            updateConversationUi();
6392            updateUnreadCountBadge();
6393            return readMessages;
6394        } else {
6395            return readMessages;
6396        }
6397    }
6398
6399    public void markNotificationDismissed(final List<Message> messages) {
6400        Runnable runnable = () -> {
6401            for (final var message : messages) {
6402                message.markNotificationDismissed();
6403                databaseBackend.updateMessage(message, false);
6404            }
6405        };
6406        mDatabaseWriterExecutor.execute(runnable);
6407    }
6408
6409    public synchronized void updateUnreadCountBadge() {
6410        int count = unreadCount();
6411        if (unreadCount != count) {
6412            Log.d(Config.LOGTAG, "update unread count to " + count);
6413            if (count > 0) {
6414                ShortcutBadger.applyCount(getApplicationContext(), count);
6415            } else {
6416                ShortcutBadger.removeCount(getApplicationContext());
6417            }
6418            unreadCount = count;
6419        }
6420    }
6421
6422    public void sendReadMarker(final Conversation conversation, final String upToUuid) {
6423        final boolean isPrivateAndNonAnonymousMuc =
6424                conversation.getMode() == Conversation.MODE_MULTI
6425                        && conversation.isPrivateAndNonAnonymous();
6426        final List<Message> readMessages = this.markRead(conversation, upToUuid, true);
6427        if (readMessages.isEmpty()) {
6428            return;
6429        }
6430        final var account = conversation.getAccount();
6431        final var connection = account.getXmppConnection();
6432        updateConversationUi();
6433        final var last =
6434                Iterables.getLast(
6435                        Collections2.filter(
6436                                readMessages,
6437                                m ->
6438                                        !m.isPrivateMessage()
6439                                                && m.getStatus() == Message.STATUS_RECEIVED),
6440                        null);
6441        if (last == null) {
6442            return;
6443        }
6444
6445        final boolean sendDisplayedMarker =
6446                confirmMessages()
6447                        && (last.trusted() || isPrivateAndNonAnonymousMuc)
6448                        && last.getRemoteMsgId() != null
6449                        && (last.markable || isPrivateAndNonAnonymousMuc);
6450        final boolean serverAssist =
6451                connection != null && connection.getFeatures().mdsServerAssist();
6452
6453        final String stanzaId = last.getServerMsgId();
6454
6455        if (sendDisplayedMarker && serverAssist) {
6456            final var mdsDisplayed = mIqGenerator.mdsDisplayed(stanzaId, conversation);
6457            final var packet = mMessageGenerator.confirm(last);
6458            packet.addChild(mdsDisplayed);
6459            if (!last.isPrivateMessage()) {
6460                packet.setTo(packet.getTo().asBareJid());
6461            }
6462            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server assisted " + packet);
6463            this.sendMessagePacket(account, packet);
6464        } else {
6465            publishMds(last);
6466            // read markers will be sent after MDS to flush the CSI stanza queue
6467            if (sendDisplayedMarker) {
6468                Log.d(
6469                        Config.LOGTAG,
6470                        conversation.getAccount().getJid().asBareJid()
6471                                + ": sending displayed marker to "
6472                                + last.getCounterpart().toString());
6473                final var packet = mMessageGenerator.confirm(last);
6474                this.sendMessagePacket(account, packet);
6475            }
6476        }
6477    }
6478
6479    private void publishMds(@Nullable final Message message) {
6480        final String stanzaId = message == null ? null : message.getServerMsgId();
6481        if (Strings.isNullOrEmpty(stanzaId)) {
6482            return;
6483        }
6484        final Conversation conversation;
6485        final var conversational = message.getConversation();
6486        if (conversational instanceof Conversation c) {
6487            conversation = c;
6488        } else {
6489            return;
6490        }
6491        final var account = conversation.getAccount();
6492        final var connection = account.getXmppConnection();
6493        if (connection == null || !connection.getFeatures().mds()) {
6494            return;
6495        }
6496        final Jid itemId;
6497        if (message.isPrivateMessage()) {
6498            itemId = message.getCounterpart();
6499        } else {
6500            itemId = conversation.getJid().asBareJid();
6501        }
6502        Log.d(Config.LOGTAG, "publishing mds for " + itemId + "/" + stanzaId);
6503        publishMds(account, itemId, stanzaId, conversation);
6504    }
6505
6506    private void publishMds(
6507            final Account account,
6508            final Jid itemId,
6509            final String stanzaId,
6510            final Conversation conversation) {
6511        final var item = mIqGenerator.mdsDisplayed(stanzaId, conversation);
6512        pushNodeAndEnforcePublishOptions(
6513                account,
6514                Namespace.MDS_DISPLAYED,
6515                item,
6516                itemId.toString(),
6517                PublishOptions.persistentWhitelistAccessMaxItems());
6518    }
6519
6520    public boolean sendReactions(final Message message, final Collection<String> reactions) {
6521        if (message.isPrivateMessage()) throw new IllegalArgumentException("Reactions to PM not implemented");
6522        if (message.getConversation() instanceof Conversation conversation) {
6523            final var isPrivateMessage = message.isPrivateMessage();
6524            final Jid reactTo;
6525            final boolean typeGroupChat;
6526            final String reactToId;
6527            final Collection<Reaction> combinedReactions;
6528            final var newReactions = new HashSet<>(reactions);
6529            newReactions.removeAll(message.getAggregatedReactions().ourReactions);
6530            if (conversation.getMode() == Conversational.MODE_MULTI && !isPrivateMessage) {
6531                final var mucOptions = conversation.getMucOptions();
6532                if (!mucOptions.participating()) {
6533                    Log.e(Config.LOGTAG, "not participating in MUC");
6534                    return false;
6535                }
6536                final var self = mucOptions.getSelf();
6537                final String occupantId = self.getOccupantId();
6538                if (Strings.isNullOrEmpty(occupantId)) {
6539                    Log.e(Config.LOGTAG, "occupant id not found for reaction in MUC");
6540                    return false;
6541                }
6542                final var existingRaw =
6543                        ImmutableSet.copyOf(
6544                                Collections2.transform(message.getReactions(), r -> r.reaction));
6545                final var reactionsAsExistingVariants =
6546                        ImmutableSet.copyOf(
6547                                Collections2.transform(
6548                                        reactions, r -> Emoticons.existingVariant(r, existingRaw)));
6549                if (!reactions.equals(reactionsAsExistingVariants)) {
6550                    Log.d(Config.LOGTAG, "modified reactions to existing variants");
6551                }
6552                reactToId = message.getServerMsgId();
6553                reactTo = conversation.getJid().asBareJid();
6554                typeGroupChat = true;
6555                combinedReactions =
6556                        Reaction.withMine(
6557                                message.getReactions(),
6558                                reactionsAsExistingVariants,
6559                                false,
6560                                self.getFullJid(),
6561                                conversation.getAccount().getJid(),
6562                                occupantId,
6563                                null);
6564            } else {
6565                if (message.isCarbon() || message.getStatus() == Message.STATUS_RECEIVED) {
6566                    reactToId = message.getRemoteMsgId();
6567                } else {
6568                    reactToId = message.getUuid();
6569                }
6570                typeGroupChat = false;
6571                if (isPrivateMessage) {
6572                    reactTo = message.getCounterpart();
6573                } else {
6574                    reactTo = conversation.getJid().asBareJid();
6575                }
6576                combinedReactions =
6577                        Reaction.withFrom(
6578                                message.getReactions(),
6579                                reactions,
6580                                false,
6581                                conversation.getAccount().getJid(),
6582                                null);
6583            }
6584            if (reactTo == null || Strings.isNullOrEmpty(reactToId)) {
6585                Log.e(Config.LOGTAG, "could not find id to react to");
6586                return false;
6587            }
6588
6589            final var packet =
6590                    mMessageGenerator.reaction(reactTo, typeGroupChat, message, reactToId, reactions);
6591
6592            final var quote = QuoteHelper.quote(MessageUtils.prepareQuote(message)) + "\n";
6593            final var body  = quote + String.join(" ", newReactions);
6594            if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && newReactions.size() > 0) {
6595                FILE_ATTACHMENT_EXECUTOR.execute(() -> {
6596                    XmppAxolotlMessage axolotlMessage = conversation.getAccount().getAxolotlService().encrypt(body, conversation);
6597                    packet.setAxolotlMessage(axolotlMessage.toElement());
6598                    packet.addChild("encryption", "urn:xmpp:eme:0")
6599                        .setAttribute("name", "OMEMO")
6600                        .setAttribute("namespace", AxolotlService.PEP_PREFIX);
6601                    sendMessagePacket(conversation.getAccount(), packet);
6602                    message.setReactions(combinedReactions);
6603                    updateMessage(message, false);
6604                });
6605            } else if (conversation.getNextEncryption() == Message.ENCRYPTION_NONE || newReactions.size() < 1) {
6606                if (newReactions.size() > 0) {
6607                    packet.setBody(body);
6608
6609                    packet.addChild("reply", "urn:xmpp:reply:0")
6610                        .setAttribute("to", message.getCounterpart())
6611                        .setAttribute("id", reactToId);
6612                    final var replyFallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reply:0");
6613                    replyFallback.addChild("body", "urn:xmpp:fallback:0")
6614                        .setAttribute("start", "0")
6615                        .setAttribute("end", "" + quote.codePointCount(0, quote.length()));
6616
6617                    final var fallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reactions:0");
6618                    fallback.addChild("body", "urn:xmpp:fallback:0");
6619                }
6620
6621                sendMessagePacket(conversation.getAccount(), packet);
6622                message.setReactions(combinedReactions);
6623                updateMessage(message, false);
6624            }
6625
6626            return true;
6627        } else {
6628            return false;
6629        }
6630    }
6631
6632    public MemorizingTrustManager getMemorizingTrustManager() {
6633        return this.mMemorizingTrustManager;
6634    }
6635
6636    public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
6637        this.mMemorizingTrustManager = trustManager;
6638    }
6639
6640    public void updateMemorizingTrustManager() {
6641        final MemorizingTrustManager trustManager;
6642        if (appSettings.isTrustSystemCAStore()) {
6643            trustManager = new MemorizingTrustManager(getApplicationContext());
6644        } else {
6645            trustManager = new MemorizingTrustManager(getApplicationContext(), null);
6646        }
6647        setMemorizingTrustManager(trustManager);
6648    }
6649
6650    public LruCache<String, Drawable> getDrawableCache() {
6651        return this.mDrawableCache;
6652    }
6653
6654    public Collection<String> getKnownHosts() {
6655        final Set<String> hosts = new HashSet<>();
6656        for (final Account account : getAccounts()) {
6657            hosts.add(account.getServer());
6658            for (final Contact contact : account.getRoster().getContacts()) {
6659                if (contact.showInRoster()) {
6660                    final String server = contact.getServer();
6661                    if (server != null) {
6662                        hosts.add(server);
6663                    }
6664                }
6665            }
6666        }
6667        if (Config.QUICKSY_DOMAIN != null) {
6668            hosts.remove(
6669                    Config.QUICKSY_DOMAIN
6670                            .toString()); // we only want to show this when we type a e164
6671            // number
6672        }
6673        if (Config.MAGIC_CREATE_DOMAIN != null) {
6674            hosts.add(Config.MAGIC_CREATE_DOMAIN);
6675        }
6676        hosts.add("chat.above.im");
6677        return hosts;
6678    }
6679
6680    public Collection<String> getKnownConferenceHosts() {
6681        final Set<String> mucServers = new HashSet<>();
6682        for (final Account account : accounts) {
6683            if (account.getXmppConnection() != null) {
6684                mucServers.addAll(account.getXmppConnection().getMucServers());
6685                for (final Bookmark bookmark : account.getBookmarks()) {
6686                    final Jid jid = bookmark.getJid();
6687                    final String s = jid == null ? null : jid.getDomain().toString();
6688                    if (s != null) {
6689                        mucServers.add(s);
6690                    }
6691                }
6692            }
6693        }
6694        return mucServers;
6695    }
6696
6697    public void sendMessagePacket(
6698            final Account account,
6699            final im.conversations.android.xmpp.model.stanza.Message packet) {
6700        final XmppConnection connection = account.getXmppConnection();
6701        if (connection != null) {
6702            connection.sendMessagePacket(packet);
6703        }
6704    }
6705
6706    public void sendPresencePacket(
6707            final Account account,
6708            final im.conversations.android.xmpp.model.stanza.Presence packet) {
6709        final XmppConnection connection = account.getXmppConnection();
6710        if (connection != null) {
6711            connection.sendPresencePacket(packet);
6712        }
6713    }
6714
6715    public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
6716        final XmppConnection connection = account.getXmppConnection();
6717        if (connection == null) {
6718            return;
6719        }
6720        connection.sendCreateAccountWithCaptchaPacket(id, data);
6721    }
6722
6723    public ListenableFuture<Iq> sendIqPacket(final Account account, final Iq request) {
6724        final XmppConnection connection = account.getXmppConnection();
6725        if (connection == null) {
6726            return Futures.immediateFailedFuture(new TimeoutException());
6727        }
6728        return connection.sendIqPacket(request);
6729    }
6730
6731    public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback) {
6732        sendIqPacket(account, packet, callback, null);
6733    }
6734
6735    public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback, Long timeout) {
6736        final XmppConnection connection = account.getXmppConnection();
6737        if (connection != null) {
6738            connection.sendIqPacket(packet, callback, timeout);
6739        } else if (callback != null) {
6740            callback.accept(Iq.TIMEOUT);
6741        }
6742    }
6743
6744    public void sendPresence(final Account account) {
6745        sendPresence(account, checkListeners() && broadcastLastActivity());
6746    }
6747
6748    private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
6749        final im.conversations.android.xmpp.model.stanza.Presence.Availability status;
6750        if (manuallyChangePresence()) {
6751            status = account.getPresenceStatus();
6752        } else {
6753            status = getTargetPresence();
6754        }
6755        final var packet = mPresenceGenerator.selfPresence(account, status);
6756        if (mLastActivity > 0 && includeIdleTimestamp) {
6757            long since =
6758                    Math.min(mLastActivity, System.currentTimeMillis()); // don't send future dates
6759            packet.addChild("idle", Namespace.IDLE)
6760                    .setAttribute("since", AbstractGenerator.getTimestamp(since));
6761        }
6762        sendPresencePacket(account, packet);
6763    }
6764
6765    private void deactivateGracePeriod() {
6766        for (Account account : getAccounts()) {
6767            account.deactivateGracePeriod();
6768        }
6769    }
6770
6771    public void refreshAllPresences() {
6772        boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
6773        for (Account account : getAccounts()) {
6774            if (account.isConnectionEnabled()) {
6775                sendPresence(account, includeIdleTimestamp);
6776            }
6777        }
6778    }
6779
6780    private void refreshAllFcmTokens() {
6781        for (Account account : getAccounts()) {
6782            if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
6783                mPushManagementService.registerPushTokenOnServer(account);
6784            }
6785        }
6786    }
6787
6788    private void sendOfflinePresence(final Account account) {
6789        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence");
6790        sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
6791    }
6792
6793    public MessageGenerator getMessageGenerator() {
6794        return this.mMessageGenerator;
6795    }
6796
6797    public PresenceGenerator getPresenceGenerator() {
6798        return this.mPresenceGenerator;
6799    }
6800
6801    public IqGenerator getIqGenerator() {
6802        return this.mIqGenerator;
6803    }
6804
6805    public JingleConnectionManager getJingleConnectionManager() {
6806        return this.mJingleConnectionManager;
6807    }
6808
6809    private boolean hasJingleRtpConnection(final Account account) {
6810        return this.mJingleConnectionManager.hasJingleRtpConnection(account);
6811    }
6812
6813    public MessageArchiveService getMessageArchiveService() {
6814        return this.mMessageArchiveService;
6815    }
6816
6817    public QuickConversationsService getQuickConversationsService() {
6818        return this.mQuickConversationsService;
6819    }
6820
6821    public List<Contact> findContacts(Jid jid, String accountJid) {
6822        ArrayList<Contact> contacts = new ArrayList<>();
6823        for (Account account : getAccounts()) {
6824            if ((account.isEnabled() || accountJid != null)
6825                    && (accountJid == null
6826                            || accountJid.equals(account.getJid().asBareJid().toString()))) {
6827                Contact contact = account.getRoster().getContactFromContactList(jid);
6828                if (contact != null) {
6829                    contacts.add(contact);
6830                }
6831            }
6832        }
6833        return contacts;
6834    }
6835
6836    public Conversation findFirstMuc(Jid jid) {
6837        return findFirstMuc(jid, null);
6838    }
6839
6840    public Conversation findFirstMuc(Jid jid, String accountJid) {
6841        for (Conversation conversation : getConversations()) {
6842            if ((conversation.getAccount().isEnabled() || accountJid != null)
6843                    && (accountJid == null || accountJid.equals(conversation.getAccount().getJid().asBareJid().toString()))
6844                    && conversation.getJid().asBareJid().equals(jid.asBareJid()) && conversation.getMode() == Conversation.MODE_MULTI) {
6845                return conversation;
6846            }
6847        }
6848        return null;
6849    }
6850
6851    public NotificationService getNotificationService() {
6852        return this.mNotificationService;
6853    }
6854
6855    public HttpConnectionManager getHttpConnectionManager() {
6856        return this.mHttpConnectionManager;
6857    }
6858
6859    public void resendFailedMessages(final Message message, final boolean forceP2P) {
6860        message.setTime(System.currentTimeMillis());
6861        markMessage(message, Message.STATUS_WAITING);
6862        this.sendMessage(message, true, false, false, forceP2P, null);
6863        if (message.getConversation() instanceof Conversation c) {
6864            c.sort();
6865        }
6866        updateConversationUi();
6867    }
6868
6869    public void clearConversationHistory(final Conversation conversation) {
6870        final long clearDate;
6871        final String reference;
6872        if (conversation.countMessages() > 0) {
6873            Message latestMessage = conversation.getLatestMessage();
6874            clearDate = latestMessage.getTimeSent() + 1000;
6875            reference = latestMessage.getServerMsgId();
6876        } else {
6877            clearDate = System.currentTimeMillis();
6878            reference = null;
6879        }
6880        conversation.clearMessages();
6881        conversation.setHasMessagesLeftOnServer(false); // avoid messages getting loaded through mam
6882        conversation.setLastClearHistory(clearDate, reference);
6883        Runnable runnable =
6884                () -> {
6885                    databaseBackend.deleteMessagesInConversation(conversation);
6886                    databaseBackend.updateConversation(conversation);
6887                };
6888        mDatabaseWriterExecutor.execute(runnable);
6889    }
6890
6891    public boolean sendBlockRequest(
6892            final Blockable blockable, final boolean reportSpam, final String serverMsgId) {
6893        if (blockable != null && blockable.getBlockedJid() != null) {
6894            final var account = blockable.getAccount();
6895            final Jid jid = blockable.getBlockedJid();
6896            this.sendIqPacket(
6897                    account,
6898                    getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId),
6899                    (response) -> {
6900                        if (response.getType() == Iq.Type.RESULT) {
6901                            account.getBlocklist().add(jid);
6902                            updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
6903                        }
6904                    });
6905            if (blockable.getBlockedJid().isFullJid()) {
6906                return false;
6907            } else if (removeBlockedConversations(blockable.getAccount(), jid)) {
6908                updateConversationUi();
6909                return true;
6910            } else {
6911                return false;
6912            }
6913        } else {
6914            return false;
6915        }
6916    }
6917
6918    public boolean removeBlockedConversations(final Account account, final Jid blockedJid) {
6919        boolean removed = false;
6920        synchronized (this.conversations) {
6921            boolean domainJid = blockedJid.getLocal() == null;
6922            for (Conversation conversation : this.conversations) {
6923                boolean jidMatches =
6924                        (domainJid
6925                                        && blockedJid
6926                                                .getDomain()
6927                                                .equals(conversation.getJid().getDomain()))
6928                                || blockedJid.equals(conversation.getJid().asBareJid());
6929                if (conversation.getAccount() == account
6930                        && conversation.getMode() == Conversation.MODE_SINGLE
6931                        && jidMatches) {
6932                    this.conversations.remove(conversation);
6933                    markRead(conversation);
6934                    conversation.setStatus(Conversation.STATUS_ARCHIVED);
6935                    Log.d(
6936                            Config.LOGTAG,
6937                            account.getJid().asBareJid()
6938                                    + ": archiving conversation "
6939                                    + conversation.getJid().asBareJid()
6940                                    + " because jid was blocked");
6941                    updateConversation(conversation);
6942                    removed = true;
6943                }
6944            }
6945        }
6946        return removed;
6947    }
6948
6949    public void sendUnblockRequest(final Blockable blockable) {
6950        if (blockable != null && blockable.getJid() != null) {
6951            final var account = blockable.getAccount();
6952            final Jid jid = blockable.getBlockedJid();
6953            this.sendIqPacket(
6954                    account,
6955                    getIqGenerator().generateSetUnblockRequest(jid),
6956                    response -> {
6957                        if (response.getType() == Iq.Type.RESULT) {
6958                            account.getBlocklist().remove(jid);
6959                            updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
6960                        }
6961                    });
6962        }
6963    }
6964
6965    public void publishDisplayName(final Account account) {
6966        String displayName = account.getDisplayName();
6967        final Iq request;
6968        if (TextUtils.isEmpty(displayName)) {
6969            request = mIqGenerator.deleteNode(Namespace.NICK);
6970        } else {
6971            request = mIqGenerator.publishNick(displayName);
6972        }
6973        mAvatarService.clear(account);
6974        sendIqPacket(
6975                account,
6976                request,
6977                (packet) -> {
6978                    if (packet.getType() == Iq.Type.ERROR) {
6979                        Log.d(
6980                                Config.LOGTAG,
6981                                account.getJid().asBareJid()
6982                                        + ": unable to modify nick name "
6983                                        + packet);
6984                    }
6985                });
6986    }
6987
6988    public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) {
6989        final var request = new Iq(input == null ? Iq.Type.GET : Iq.Type.SET);
6990        request.setTo(jid);
6991        Element query = request.query("jabber:iq:gateway");
6992        if (input != null) {
6993            Element prompt = query.addChild("prompt");
6994            prompt.setContent(input);
6995        }
6996        sendIqPacket(account, request, packet -> {
6997            if (packet.getType() == Iq.Type.RESULT) {
6998                callback.onGatewayResult(packet.query().findChildContent(input == null ? "prompt" : "jid"), null);
6999            } else {
7000                Element error = packet.findChild("error");
7001                callback.onGatewayResult(null, error == null ? null : error.findChildContent("text"));
7002            }
7003        });
7004    }
7005
7006    public void fetchMamPreferences(final Account account, final OnMamPreferencesFetched callback) {
7007        final MessageArchiveService.Version version = MessageArchiveService.Version.get(account);
7008        final Iq request = new Iq(Iq.Type.GET);
7009        request.addChild("prefs", version.namespace);
7010        sendIqPacket(
7011                account,
7012                request,
7013                (packet) -> {
7014                    final Element prefs = packet.findChild("prefs", version.namespace);
7015                    if (packet.getType() == Iq.Type.RESULT && prefs != null) {
7016                        callback.onPreferencesFetched(prefs);
7017                    } else {
7018                        callback.onPreferencesFetchFailed();
7019                    }
7020                });
7021    }
7022
7023    public PushManagementService getPushManagementService() {
7024        return mPushManagementService;
7025    }
7026
7027    public void changeStatus(Account account, PresenceTemplate template, String signature) {
7028        if (!template.getStatusMessage().isEmpty()) {
7029            databaseBackend.insertPresenceTemplate(template);
7030        }
7031        account.setPgpSignature(signature);
7032        account.setPresenceStatus(template.getStatus());
7033        account.setPresenceStatusMessage(template.getStatusMessage());
7034        databaseBackend.updateAccount(account);
7035        sendPresence(account);
7036    }
7037
7038    public List<PresenceTemplate> getPresenceTemplates(Account account) {
7039        List<PresenceTemplate> templates = databaseBackend.getPresenceTemplates();
7040        for (PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) {
7041            if (!templates.contains(template)) {
7042                templates.add(0, template);
7043            }
7044        }
7045        return templates;
7046    }
7047
7048    public void saveConversationAsBookmark(final Conversation conversation, final String name) {
7049        final Account account = conversation.getAccount();
7050        final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid());
7051        String nick = conversation.getMucOptions().getActualNick();
7052        if (nick == null) nick = conversation.getJid().getResource();
7053        if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
7054            bookmark.setNick(nick);
7055        }
7056        if (!TextUtils.isEmpty(name)) {
7057            bookmark.setBookmarkName(name);
7058        }
7059        bookmark.setAutojoin(true);
7060        createBookmark(account, bookmark);
7061        bookmark.setConversation(conversation);
7062    }
7063
7064    public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
7065        boolean performedVerification = false;
7066        final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
7067        for (XmppUri.Fingerprint fp : fingerprints) {
7068            if (fp.type == XmppUri.FingerprintType.OMEMO) {
7069                String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
7070                FingerprintStatus fingerprintStatus =
7071                        axolotlService.getFingerprintTrust(fingerprint);
7072                if (fingerprintStatus != null) {
7073                    if (!fingerprintStatus.isVerified()) {
7074                        performedVerification = true;
7075                        axolotlService.setFingerprintTrust(
7076                                fingerprint, fingerprintStatus.toVerified());
7077                    }
7078                } else {
7079                    axolotlService.preVerifyFingerprint(contact, fingerprint);
7080                }
7081            }
7082        }
7083        return performedVerification;
7084    }
7085
7086    public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
7087        final AxolotlService axolotlService = account.getAxolotlService();
7088        boolean verifiedSomething = false;
7089        for (XmppUri.Fingerprint fp : fingerprints) {
7090            if (fp.type == XmppUri.FingerprintType.OMEMO) {
7091                String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
7092                Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint);
7093                FingerprintStatus fingerprintStatus =
7094                        axolotlService.getFingerprintTrust(fingerprint);
7095                if (fingerprintStatus != null) {
7096                    if (!fingerprintStatus.isVerified()) {
7097                        axolotlService.setFingerprintTrust(
7098                                fingerprint, fingerprintStatus.toVerified());
7099                        verifiedSomething = true;
7100                    }
7101                } else {
7102                    axolotlService.preVerifyFingerprint(account, fingerprint);
7103                    verifiedSomething = true;
7104                }
7105            }
7106        }
7107        return verifiedSomething;
7108    }
7109
7110    public ShortcutService getShortcutService() {
7111        return mShortcutService;
7112    }
7113
7114    public void pushMamPreferences(Account account, Element prefs) {
7115        final Iq set = new Iq(Iq.Type.SET);
7116        set.addChild(prefs);
7117        account.setMamPrefs(prefs);
7118        sendIqPacket(account, set, null);
7119    }
7120
7121    public void evictPreview(File f) {
7122        if (f == null) return;
7123
7124        if (mDrawableCache.remove(f.getAbsolutePath()) != null) {
7125            Log.d(Config.LOGTAG, "deleted cached preview");
7126        }
7127    }
7128
7129    public void evictPreview(String uuid) {
7130        if (mDrawableCache.remove(uuid) != null) {
7131            Log.d(Config.LOGTAG, "deleted cached preview");
7132        }
7133    }
7134
7135    public interface OnMamPreferencesFetched {
7136        void onPreferencesFetched(Element prefs);
7137
7138        void onPreferencesFetchFailed();
7139    }
7140
7141    public interface OnAccountCreated {
7142        void onAccountCreated(Account account);
7143
7144        void informUser(int r);
7145    }
7146
7147    public interface OnMoreMessagesLoaded {
7148        void onMoreMessagesLoaded(int count, Conversation conversation);
7149
7150        void informUser(int r);
7151    }
7152
7153    public interface OnAccountPasswordChanged {
7154        void onPasswordChangeSucceeded();
7155
7156        void onPasswordChangeFailed();
7157    }
7158
7159    public interface OnRoomDestroy {
7160        void onRoomDestroySucceeded();
7161
7162        void onRoomDestroyFailed();
7163    }
7164
7165    public interface OnAffiliationChanged {
7166        void onAffiliationChangedSuccessful(Jid jid);
7167
7168        void onAffiliationChangeFailed(Jid jid, int resId);
7169    }
7170
7171    public interface OnConversationUpdate {
7172        default void onConversationUpdate() { onConversationUpdate(false); }
7173        default void onConversationUpdate(boolean newCaps) { onConversationUpdate(); }
7174    }
7175
7176    public interface OnJingleRtpConnectionUpdate {
7177        void onJingleRtpConnectionUpdate(
7178                final Account account,
7179                final Jid with,
7180                final String sessionId,
7181                final RtpEndUserState state);
7182
7183        void onAudioDeviceChanged(
7184                CallIntegration.AudioDevice selectedAudioDevice,
7185                Set<CallIntegration.AudioDevice> availableAudioDevices);
7186    }
7187
7188    public interface OnAccountUpdate {
7189        void onAccountUpdate();
7190    }
7191
7192    public interface OnCaptchaRequested {
7193        void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha);
7194    }
7195
7196    public interface OnRosterUpdate {
7197        void onRosterUpdate(final UpdateRosterReason reason, final Contact contact);
7198    }
7199
7200    public interface OnMucRosterUpdate {
7201        void onMucRosterUpdate();
7202    }
7203
7204    public interface OnConferenceConfigurationFetched {
7205        void onConferenceConfigurationFetched(Conversation conversation);
7206
7207        void onFetchFailed(Conversation conversation, String errorCondition);
7208    }
7209
7210    public interface OnConferenceJoined {
7211        void onConferenceJoined(Conversation conversation);
7212    }
7213
7214    public interface OnConfigurationPushed {
7215        void onPushSucceeded();
7216
7217        void onPushFailed();
7218    }
7219
7220    public interface OnShowErrorToast {
7221        void onShowErrorToast(int resId);
7222    }
7223
7224    public class XmppConnectionBinder extends Binder {
7225        public XmppConnectionService getService() {
7226            return XmppConnectionService.this;
7227        }
7228    }
7229
7230    private class InternalEventReceiver extends BroadcastReceiver {
7231
7232        @Override
7233        public void onReceive(final Context context, final Intent intent) {
7234            onStartCommand(intent, 0, 0);
7235        }
7236    }
7237
7238    private class RestrictedEventReceiver extends BroadcastReceiver {
7239
7240        private final Collection<String> allowedActions;
7241
7242        private RestrictedEventReceiver(final Collection<String> allowedActions) {
7243            this.allowedActions = allowedActions;
7244        }
7245
7246        @Override
7247        public void onReceive(final Context context, final Intent intent) {
7248            final String action = intent == null ? null : intent.getAction();
7249            if (allowedActions.contains(action)) {
7250                onStartCommand(intent, 0, 0);
7251            } else {
7252                Log.e(Config.LOGTAG, "restricting broadcast of event " + action);
7253            }
7254        }
7255    }
7256
7257    public static class OngoingCall {
7258        public final AbstractJingleConnection.Id id;
7259        public final Set<Media> media;
7260        public final boolean reconnecting;
7261
7262        public OngoingCall(
7263                AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
7264            this.id = id;
7265            this.media = media;
7266            this.reconnecting = reconnecting;
7267        }
7268
7269        @Override
7270        public boolean equals(Object o) {
7271            if (this == o) return true;
7272            if (o == null || getClass() != o.getClass()) return false;
7273            OngoingCall that = (OngoingCall) o;
7274            return reconnecting == that.reconnecting
7275                    && Objects.equal(id, that.id)
7276                    && Objects.equal(media, that.media);
7277        }
7278
7279        @Override
7280        public int hashCode() {
7281            return Objects.hashCode(id, media, reconnecting);
7282        }
7283    }
7284
7285    public static void toggleForegroundService(final XmppConnectionService service) {
7286        if (service == null) {
7287            return;
7288        }
7289        service.toggleForegroundService();
7290    }
7291
7292    public static void toggleForegroundService(final ConversationsActivity activity) {
7293        if (activity == null) {
7294            return;
7295        }
7296        toggleForegroundService(activity.xmppConnectionService);
7297    }
7298
7299    public static class BlockedMediaException extends Exception { }
7300
7301    public static enum UpdateRosterReason {
7302        INIT,
7303        AVATAR,
7304        PUSH,
7305        PRESENCE
7306    }
7307}