XmppConnectionService.java

   1package eu.siacs.conversations.services;
   2
   3import static eu.siacs.conversations.utils.Compatibility.s;
   4
   5import android.Manifest;
   6import android.annotation.SuppressLint;
   7import android.app.AlarmManager;
   8import android.app.Notification;
   9import android.app.NotificationManager;
  10import android.app.PendingIntent;
  11import android.app.Service;
  12import android.content.BroadcastReceiver;
  13import android.content.ComponentName;
  14import android.content.Context;
  15import android.content.Intent;
  16import android.content.IntentFilter;
  17import android.content.SharedPreferences;
  18import android.content.pm.PackageManager;
  19import android.content.pm.ServiceInfo;
  20import android.database.ContentObserver;
  21import android.graphics.Bitmap;
  22import android.graphics.drawable.AnimatedImageDrawable;
  23import android.graphics.drawable.BitmapDrawable;
  24import android.graphics.drawable.Drawable;
  25import android.media.AudioManager;
  26import android.net.ConnectivityManager;
  27import android.net.Network;
  28import android.net.NetworkCapabilities;
  29import android.net.NetworkInfo;
  30import android.net.Uri;
  31import android.os.Binder;
  32import android.os.Build;
  33import android.os.Bundle;
  34import android.os.Environment;
  35import android.os.IBinder;
  36import android.os.Messenger;
  37import android.os.PowerManager;
  38import android.os.PowerManager.WakeLock;
  39import android.os.SystemClock;
  40import android.preference.PreferenceManager;
  41import android.provider.ContactsContract;
  42import android.provider.DocumentsContract;
  43import android.security.KeyChain;
  44import android.util.Log;
  45import android.util.LruCache;
  46import android.util.Pair;
  47import androidx.annotation.IntegerRes;
  48import androidx.annotation.NonNull;
  49import androidx.annotation.Nullable;
  50import androidx.core.app.RemoteInput;
  51import androidx.core.content.ContextCompat;
  52
  53import com.cheogram.android.EmojiSearch;
  54import com.cheogram.android.WebxdcUpdate;
  55
  56import com.google.common.base.Objects;
  57import com.google.common.base.Optional;
  58import com.google.common.base.Strings;
  59import com.google.common.collect.Collections2;
  60import com.google.common.collect.ImmutableMap;
  61import com.google.common.collect.ImmutableSet;
  62import com.google.common.collect.Iterables;
  63import com.google.common.collect.Maps;
  64import com.google.common.collect.Multimap;
  65import com.google.common.io.Files;
  66
  67import com.kedia.ogparser.JsoupProxy;
  68import com.kedia.ogparser.OpenGraphCallback;
  69import com.kedia.ogparser.OpenGraphParser;
  70import com.kedia.ogparser.OpenGraphResult;
  71
  72import org.conscrypt.Conscrypt;
  73import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep;
  74import org.openintents.openpgp.IOpenPgpService2;
  75import org.openintents.openpgp.util.OpenPgpApi;
  76import org.openintents.openpgp.util.OpenPgpServiceConnection;
  77
  78import java.io.File;
  79import java.io.FileInputStream;
  80import java.io.IOException;
  81import java.net.URI;
  82import java.security.NoSuchAlgorithmException;
  83import java.security.Security;
  84import java.security.cert.CertificateException;
  85import java.security.cert.X509Certificate;
  86import java.util.ArrayList;
  87import java.util.Arrays;
  88import java.util.Collection;
  89import java.util.Collections;
  90import java.util.HashMap;
  91import java.util.HashSet;
  92import java.util.Hashtable;
  93import java.util.Iterator;
  94import java.util.List;
  95import java.util.ListIterator;
  96import java.util.Map;
  97import java.util.Set;
  98import java.util.WeakHashMap;
  99import java.util.concurrent.CopyOnWriteArrayList;
 100import java.util.concurrent.CountDownLatch;
 101import java.util.concurrent.Executor;
 102import java.util.concurrent.Executors;
 103import java.util.concurrent.Semaphore;
 104import java.util.concurrent.RejectedExecutionException;
 105import java.util.concurrent.ScheduledExecutorService;
 106import java.util.concurrent.TimeUnit;
 107import java.util.concurrent.atomic.AtomicBoolean;
 108import java.util.concurrent.atomic.AtomicLong;
 109import java.util.concurrent.atomic.AtomicReference;
 110import java.util.function.Consumer;
 111
 112import io.ipfs.cid.Cid;
 113
 114import com.google.common.util.concurrent.FutureCallback;
 115import com.google.common.util.concurrent.Futures;
 116import com.google.common.util.concurrent.ListenableFuture;
 117import com.google.common.util.concurrent.MoreExecutors;
 118import eu.siacs.conversations.AppSettings;
 119import eu.siacs.conversations.Config;
 120import eu.siacs.conversations.R;
 121import eu.siacs.conversations.android.JabberIdContact;
 122import eu.siacs.conversations.crypto.OmemoSetting;
 123import eu.siacs.conversations.crypto.PgpDecryptionService;
 124import eu.siacs.conversations.crypto.PgpEngine;
 125import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 126import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 127import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
 128import eu.siacs.conversations.entities.Account;
 129import eu.siacs.conversations.entities.Blockable;
 130import eu.siacs.conversations.entities.Bookmark;
 131import eu.siacs.conversations.entities.Contact;
 132import eu.siacs.conversations.entities.Conversation;
 133import eu.siacs.conversations.entities.Conversational;
 134import eu.siacs.conversations.entities.DownloadableFile;
 135import eu.siacs.conversations.entities.Message;
 136import eu.siacs.conversations.entities.MucOptions;
 137import eu.siacs.conversations.entities.PresenceTemplate;
 138import eu.siacs.conversations.entities.Reaction;
 139import eu.siacs.conversations.generator.AbstractGenerator;
 140import eu.siacs.conversations.generator.IqGenerator;
 141import eu.siacs.conversations.generator.MessageGenerator;
 142import eu.siacs.conversations.http.HttpConnectionManager;
 143import eu.siacs.conversations.http.ServiceOutageStatus;
 144import eu.siacs.conversations.parser.IqParser;
 145import eu.siacs.conversations.persistance.DatabaseBackend;
 146import eu.siacs.conversations.persistance.FileBackend;
 147import eu.siacs.conversations.persistance.UnifiedPushDatabase;
 148import eu.siacs.conversations.receiver.SystemEventReceiver;
 149import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity;
 150import eu.siacs.conversations.ui.ConversationsActivity;
 151import eu.siacs.conversations.ui.RtpSessionActivity;
 152import eu.siacs.conversations.ui.UiCallback;
 153import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
 154import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
 155import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
 156import eu.siacs.conversations.ui.util.QuoteHelper;
 157import eu.siacs.conversations.utils.AccountUtils;
 158import eu.siacs.conversations.utils.Compatibility;
 159import eu.siacs.conversations.utils.ConversationsFileObserver;
 160import eu.siacs.conversations.utils.CryptoHelper;
 161import eu.siacs.conversations.utils.Emoticons;
 162import eu.siacs.conversations.utils.EasyOnboardingInvite;
 163import eu.siacs.conversations.utils.ExceptionHelper;
 164import eu.siacs.conversations.utils.FileUtils;
 165import eu.siacs.conversations.utils.MessageUtils;
 166import eu.siacs.conversations.utils.Emoticons;
 167import eu.siacs.conversations.utils.MimeUtils;
 168import eu.siacs.conversations.utils.PhoneHelper;
 169import eu.siacs.conversations.utils.QuickLoader;
 170import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
 171import eu.siacs.conversations.utils.Resolver;
 172import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
 173import eu.siacs.conversations.utils.TorServiceUtils;
 174import eu.siacs.conversations.utils.ThemeHelper;
 175import eu.siacs.conversations.utils.WakeLockHelper;
 176import eu.siacs.conversations.utils.XmppUri;
 177import eu.siacs.conversations.xml.Element;
 178import eu.siacs.conversations.xml.LocalizedContent;
 179import eu.siacs.conversations.xml.Namespace;
 180import eu.siacs.conversations.xmpp.Jid;
 181import eu.siacs.conversations.xmpp.OnContactStatusChanged;
 182import eu.siacs.conversations.xmpp.OnGatewayResult;
 183import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 184import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 185import eu.siacs.conversations.xmpp.XmppConnection;
 186import eu.siacs.conversations.xmpp.chatstate.ChatState;
 187import eu.siacs.conversations.xmpp.forms.Data;
 188import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
 189import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
 190import eu.siacs.conversations.xmpp.jingle.Media;
 191import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
 192import eu.siacs.conversations.xmpp.mam.MamReference;
 193import eu.siacs.conversations.xmpp.manager.AvatarManager;
 194import eu.siacs.conversations.xmpp.manager.BlockingManager;
 195import eu.siacs.conversations.xmpp.manager.BookmarkManager;
 196import eu.siacs.conversations.xmpp.manager.DiscoManager;
 197import eu.siacs.conversations.xmpp.manager.MessageDisplayedSynchronizationManager;
 198import eu.siacs.conversations.xmpp.manager.MultiUserChatManager;
 199import eu.siacs.conversations.xmpp.manager.NickManager;
 200import eu.siacs.conversations.xmpp.manager.PepManager;
 201import eu.siacs.conversations.xmpp.manager.PresenceManager;
 202import eu.siacs.conversations.xmpp.manager.RegistrationManager;
 203import eu.siacs.conversations.xmpp.manager.RosterManager;
 204import eu.siacs.conversations.xmpp.manager.VCardManager;
 205import eu.siacs.conversations.xmpp.pep.Avatar;
 206import im.conversations.android.xmpp.Entity;
 207import im.conversations.android.xmpp.model.disco.info.InfoQuery;
 208import im.conversations.android.xmpp.model.muc.Affiliation;
 209import im.conversations.android.xmpp.model.muc.Role;
 210import im.conversations.android.xmpp.model.stanza.Iq;
 211import im.conversations.android.xmpp.model.stanza.Presence;
 212import im.conversations.android.xmpp.model.storage.PrivateStorage;
 213import im.conversations.android.xmpp.model.up.Push;
 214import java.io.File;
 215import java.security.Security;
 216import java.security.cert.CertificateException;
 217import java.security.cert.X509Certificate;
 218import java.util.ArrayList;
 219import java.util.Arrays;
 220import java.util.Collection;
 221import java.util.Collections;
 222import java.util.HashSet;
 223import java.util.Iterator;
 224import java.util.List;
 225import java.util.Map;
 226import java.util.Set;
 227import java.util.WeakHashMap;
 228import java.util.concurrent.CopyOnWriteArrayList;
 229import java.util.concurrent.CountDownLatch;
 230import java.util.concurrent.Executor;
 231import java.util.concurrent.Executors;
 232import java.util.concurrent.RejectedExecutionException;
 233import java.util.concurrent.ScheduledExecutorService;
 234import java.util.concurrent.TimeUnit;
 235import java.util.concurrent.TimeoutException;
 236import java.util.concurrent.atomic.AtomicBoolean;
 237import java.util.concurrent.atomic.AtomicLong;
 238import java.util.concurrent.atomic.AtomicReference;
 239import java.util.function.Consumer;
 240import me.leolin.shortcutbadger.ShortcutBadger;
 241import okhttp3.HttpUrl;
 242import org.conscrypt.Conscrypt;
 243import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep;
 244import org.openintents.openpgp.IOpenPgpService2;
 245import org.openintents.openpgp.util.OpenPgpApi;
 246import org.openintents.openpgp.util.OpenPgpServiceConnection;
 247
 248import okhttp3.HttpUrl;
 249import okhttp3.OkHttpClient;
 250
 251public class XmppConnectionService extends Service {
 252
 253    public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations";
 254    public static final String ACTION_MARK_AS_READ = "mark_as_read";
 255    public static final String ACTION_SNOOZE = "snooze";
 256    public static final String ACTION_CLEAR_MESSAGE_NOTIFICATION = "clear_message_notification";
 257    public static final String ACTION_CLEAR_MISSED_CALL_NOTIFICATION =
 258            "clear_missed_call_notification";
 259    public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error";
 260    public static final String ACTION_TRY_AGAIN = "try_again";
 261
 262    public static final String ACTION_TEMPORARILY_DISABLE = "temporarily_disable";
 263    public static final String ACTION_PING = "ping";
 264    public static final String ACTION_IDLE_PING = "idle_ping";
 265    public static final String ACTION_INTERNAL_PING = "internal_ping";
 266    public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh";
 267    public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received";
 268    public static final String ACTION_DISMISS_CALL = "dismiss_call";
 269    public static final String ACTION_END_CALL = "end_call";
 270    public static final String ACTION_STARTING_CALL = "starting_call";
 271    public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
 272    public static final String ACTION_CALL_INTEGRATION_SERVICE_STARTED =
 273            "call_integration_service_started";
 274    private static final String ACTION_POST_CONNECTIVITY_CHANGE =
 275            "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
 276    public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS =
 277            "eu.siacs.conversations.UNIFIED_PUSH_RENEW";
 278    public static final String ACTION_QUICK_LOG = "eu.siacs.conversations.QUICK_LOG";
 279
 280    private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
 281
 282    public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
 283    private static final Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor();
 284    public static final Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor();
 285
 286    public final ScheduledExecutorService internalPingExecutor =
 287            Executors.newSingleThreadScheduledExecutor();
 288    private static final SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR =
 289            new SerialSingleThreadExecutor("VideoCompression");
 290    private final SerialSingleThreadExecutor mDatabaseWriterExecutor =
 291            new SerialSingleThreadExecutor("DatabaseWriter");
 292    private final SerialSingleThreadExecutor mDatabaseReaderExecutor =
 293            new SerialSingleThreadExecutor("DatabaseReader");
 294    private final SerialSingleThreadExecutor mNotificationExecutor =
 295            new SerialSingleThreadExecutor("NotificationExecutor");
 296    private final IBinder mBinder = new XmppConnectionBinder();
 297    private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
 298    private final IqGenerator mIqGenerator = new IqGenerator(this);
 299    private final Set<String> mInProgressAvatarFetches = new HashSet<>();
 300    private final Set<String> mOmittedPepAvatarFetches = new HashSet<>();
 301    public final HashSet<Jid> mLowPingTimeoutMode = new HashSet<>();
 302    public DatabaseBackend databaseBackend;
 303    private Multimap<String, String> mutedMucUsers;
 304    private final ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger");
 305    private final ReplacingSerialSingleThreadExecutor mStickerScanExecutor = new ReplacingSerialSingleThreadExecutor("StickerScan");
 306    private long mLastActivity = 0;
 307    private long mLastMucPing = 0;
 308    private Map<String, Message> mScheduledMessages = new HashMap<>();
 309    private long mLastStickerRescan = 0;
 310    private final AppSettings appSettings = new AppSettings(this);
 311    private final FileBackend fileBackend = new FileBackend(this);
 312    private MemorizingTrustManager mMemorizingTrustManager;
 313    private final NotificationService mNotificationService = new NotificationService(this);
 314    private final UnifiedPushBroker unifiedPushBroker = new UnifiedPushBroker(this);
 315    private final ChannelDiscoveryService mChannelDiscoveryService =
 316            new ChannelDiscoveryService(this);
 317    private final ShortcutService mShortcutService = new ShortcutService(this);
 318    private final AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false);
 319    private final AtomicBoolean mOngoingVideoTranscoding = new AtomicBoolean(false);
 320    private final AtomicBoolean mForceDuringOnCreate = new AtomicBoolean(false);
 321    private final AtomicReference<OngoingCall> ongoingCall = new AtomicReference<>();
 322    private final MessageGenerator mMessageGenerator = new MessageGenerator(this);
 323    public OnContactStatusChanged onContactStatusChanged =
 324            (contact, online) -> {
 325                final var conversation = find(contact);
 326                if (conversation == null) {
 327                    return;
 328                }
 329                if (online) {
 330                    if (contact.getPresences().size() == 1) {
 331                        sendUnsentMessages(conversation);
 332                    }
 333                }
 334            };
 335    private List<Account> accounts;
 336    private final JingleConnectionManager mJingleConnectionManager =
 337            new JingleConnectionManager(this);
 338    private final HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(this);
 339    private final AvatarService mAvatarService = new AvatarService(this);
 340    private final MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
 341    private final PushManagementService mPushManagementService = new PushManagementService(this);
 342    private final QuickConversationsService mQuickConversationsService =
 343            new QuickConversationsService(this);
 344    private final ConversationsFileObserver fileObserver =
 345            new ConversationsFileObserver(
 346                    Environment.getExternalStorageDirectory().getAbsolutePath()) {
 347                @Override
 348                public void onEvent(final int event, final File file) {
 349                    markFileDeleted(file);
 350                }
 351            };
 352
 353    private final AtomicBoolean diallerIntegrationActive = new AtomicBoolean(false);
 354
 355    public void setDiallerIntegrationActive(boolean active) {
 356      diallerIntegrationActive.set(active);
 357    }
 358
 359    private boolean destroyed = false;
 360
 361    private int unreadCount = -1;
 362
 363    // Ui callback listeners
 364    private final Set<OnConversationUpdate> mOnConversationUpdates =
 365            Collections.newSetFromMap(new WeakHashMap<OnConversationUpdate, Boolean>());
 366    private final Set<OnShowErrorToast> mOnShowErrorToasts =
 367            Collections.newSetFromMap(new WeakHashMap<OnShowErrorToast, Boolean>());
 368    private final Set<OnAccountUpdate> mOnAccountUpdates =
 369            Collections.newSetFromMap(new WeakHashMap<OnAccountUpdate, Boolean>());
 370    private final Set<OnCaptchaRequested> mOnCaptchaRequested =
 371            Collections.newSetFromMap(new WeakHashMap<OnCaptchaRequested, Boolean>());
 372    private final Set<OnRosterUpdate> mOnRosterUpdates =
 373            Collections.newSetFromMap(new WeakHashMap<OnRosterUpdate, Boolean>());
 374    private final Set<OnUpdateBlocklist> mOnUpdateBlocklist =
 375            Collections.newSetFromMap(new WeakHashMap<OnUpdateBlocklist, Boolean>());
 376    private final Set<OnMucRosterUpdate> mOnMucRosterUpdate =
 377            Collections.newSetFromMap(new WeakHashMap<OnMucRosterUpdate, Boolean>());
 378    private final Set<OnKeyStatusUpdated> mOnKeyStatusUpdated =
 379            Collections.newSetFromMap(new WeakHashMap<OnKeyStatusUpdated, Boolean>());
 380    private final Set<OnJingleRtpConnectionUpdate> onJingleRtpConnectionUpdate =
 381            Collections.newSetFromMap(new WeakHashMap<OnJingleRtpConnectionUpdate, Boolean>());
 382
 383    private final Object LISTENER_LOCK = new Object();
 384
 385    public final Set<String> FILENAMES_TO_IGNORE_DELETION = new HashSet<>();
 386
 387    private final AtomicLong mLastExpiryRun = new AtomicLong(0);
 388
 389    private OpenPgpServiceConnection pgpServiceConnection;
 390    private PgpEngine mPgpEngine = null;
 391    private WakeLock wakeLock;
 392    private LruCache<String, Drawable> mDrawableCache;
 393    private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
 394    private final BroadcastReceiver mInternalRestrictedEventReceiver =
 395            new RestrictedEventReceiver(List.of(TorServiceUtils.ACTION_STATUS));
 396    private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
 397    private EmojiSearch emojiSearch = null;
 398
 399    private static String generateFetchKey(Account account, final Avatar avatar) {
 400        return account.getJid().asBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum;
 401    }
 402
 403    public boolean isInLowPingTimeoutMode(Account account) {
 404        synchronized (mLowPingTimeoutMode) {
 405            return mLowPingTimeoutMode.contains(account.getJid().asBareJid());
 406        }
 407    }
 408
 409    public void startOngoingVideoTranscodingForegroundNotification() {
 410        mOngoingVideoTranscoding.set(true);
 411        toggleForegroundService();
 412    }
 413
 414    public void stopOngoingVideoTranscodingForegroundNotification() {
 415        mOngoingVideoTranscoding.set(false);
 416        toggleForegroundService();
 417    }
 418
 419    public boolean areMessagesInitialized() {
 420        return this.restoredFromDatabaseLatch.getCount() == 0;
 421    }
 422
 423    public PgpEngine getPgpEngine() {
 424        if (!Config.supportOpenPgp()) {
 425            return null;
 426        } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
 427            if (this.mPgpEngine == null) {
 428                this.mPgpEngine =
 429                        new PgpEngine(
 430                                new OpenPgpApi(
 431                                        getApplicationContext(), pgpServiceConnection.getService()),
 432                                this);
 433            }
 434            return mPgpEngine;
 435        } else {
 436            return null;
 437        }
 438    }
 439
 440    public OpenPgpApi getOpenPgpApi() {
 441        if (!Config.supportOpenPgp()) {
 442            return null;
 443        } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
 444            return new OpenPgpApi(this, pgpServiceConnection.getService());
 445        } else {
 446            return null;
 447        }
 448    }
 449
 450    public AppSettings getAppSettings() {
 451        return this.appSettings;
 452    }
 453
 454    public FileBackend getFileBackend() {
 455        return this.fileBackend;
 456    }
 457
 458    public DownloadableFile getFileForCid(Cid cid) {
 459        return this.databaseBackend.getFileForCid(cid);
 460    }
 461
 462    public String getUrlForCid(Cid cid) {
 463        return this.databaseBackend.getUrlForCid(cid);
 464    }
 465
 466    public void saveCid(Cid cid, File file) throws BlockedMediaException {
 467        saveCid(cid, file, null);
 468    }
 469
 470    public void saveCid(Cid cid, File file, String url) throws BlockedMediaException {
 471        if (this.databaseBackend.isBlockedMedia(cid)) {
 472            throw new BlockedMediaException();
 473        }
 474        this.databaseBackend.saveCid(cid, file, url);
 475    }
 476
 477    public boolean muteMucUser(MucOptions.User user) {
 478        boolean muted = databaseBackend.muteMucUser(user);
 479        if (!muted) return false;
 480        mutedMucUsers.put(user.getMuc().toString(), user.getOccupantId());
 481        return true;
 482    }
 483
 484    public boolean unmuteMucUser(MucOptions.User user) {
 485        boolean unmuted = databaseBackend.unmuteMucUser(user);
 486        if (!unmuted) return false;
 487        mutedMucUsers.remove(user.getMuc().toString(), user.getOccupantId());
 488        return true;
 489    }
 490
 491    public boolean isMucUserMuted(MucOptions.User user) {
 492        return mutedMucUsers.containsEntry("" + user.getMuc(), user.getOccupantId());
 493    }
 494
 495    public void blockMedia(File f) {
 496        try {
 497            Cid[] cids = getFileBackend().calculateCids(new FileInputStream(f));
 498            for (Cid cid : cids) {
 499                blockMedia(cid);
 500            }
 501        } catch (final IOException e) { }
 502    }
 503
 504    public void blockMedia(Cid cid) {
 505        this.databaseBackend.blockMedia(cid);
 506    }
 507
 508    public boolean isBlockedMedia(Cid cid) {
 509        return databaseBackend.isBlockedMedia(cid);
 510    }
 511
 512    public boolean isBlockedMediaSha1(@Nullable final String sha1sum) {
 513        if (sha1sum == null) return false;
 514        try {
 515            return isBlockedMedia(CryptoHelper.cid(CryptoHelper.hexToBytes(sha1sum), "sha-1"));
 516        } catch (final NoSuchAlgorithmException e) {
 517            return false;
 518        }
 519    }
 520
 521    public void clearBlockedMedia() {
 522        this.databaseBackend.clearBlockedMedia();
 523    }
 524
 525    public Message getMessage(Conversation conversation, String uuid) {
 526        return this.databaseBackend.getMessage(conversation, uuid);
 527    }
 528
 529    public Map<String, Message> getMessageFuzzyIds(Conversation conversation, Collection<String> ids) {
 530        return this.databaseBackend.getMessageFuzzyIds(conversation, ids);
 531    }
 532
 533    public void insertWebxdcUpdate(final WebxdcUpdate update) {
 534        this.databaseBackend.insertWebxdcUpdate(update);
 535    }
 536
 537    public WebxdcUpdate findLastWebxdcUpdate(Message message) {
 538        return this.databaseBackend.findLastWebxdcUpdate(message);
 539    }
 540
 541    public List<WebxdcUpdate> findWebxdcUpdates(Message message, long serial) {
 542        return this.databaseBackend.findWebxdcUpdates(message, serial);
 543    }
 544
 545    public AvatarService getAvatarService() {
 546        return this.mAvatarService;
 547    }
 548
 549    public void attachLocationToConversation(
 550            final Conversation conversation, final Uri uri, final String subject, final UiCallback<Message> callback) {
 551        int encryption = conversation.getNextEncryption();
 552        if (encryption == Message.ENCRYPTION_PGP) {
 553            encryption = Message.ENCRYPTION_DECRYPTED;
 554        }
 555        Message message = new Message(conversation, uri.toString(), encryption);
 556        if (subject != null && subject.length() > 0) message.setSubject(subject);
 557        message.setThread(conversation.getThread());
 558        Message.configurePrivateMessage(message);
 559        if (encryption == Message.ENCRYPTION_DECRYPTED) {
 560            getPgpEngine().encrypt(message, callback);
 561        } else {
 562            sendMessage(message);
 563            callback.success(message);
 564        }
 565    }
 566
 567    public void attachFileToConversation(
 568            final Conversation conversation,
 569            final Uri uri,
 570            final String type,
 571            final String subject,
 572            final UiCallback<Message> callback) {
 573        final Message message;
 574        if (conversation.getReplyTo() == null) {
 575            message = new Message(conversation, "", conversation.getNextEncryption());
 576        } else {
 577            message = conversation.getReplyTo().reply();
 578            message.setEncryption(conversation.getNextEncryption());
 579        }
 580        if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 581            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 582        }
 583        if (subject != null && subject.length() > 0) message.setSubject(subject);
 584        message.setThread(conversation.getThread());
 585        if (!Message.configurePrivateFileMessage(message)) {
 586            message.setCounterpart(conversation.getNextCounterpart());
 587            message.setType(Message.TYPE_FILE);
 588        }
 589        Log.d(Config.LOGTAG, "attachFile: type=" + message.getType());
 590        Log.d(Config.LOGTAG, "counterpart=" + message.getCounterpart());
 591        final AttachFileToConversationRunnable runnable =
 592                new AttachFileToConversationRunnable(this, uri, type, message, callback);
 593        if (runnable.isVideoMessage()) {
 594            VIDEO_COMPRESSION_EXECUTOR.execute(runnable);
 595        } else {
 596            FILE_ATTACHMENT_EXECUTOR.execute(runnable);
 597        }
 598    }
 599
 600    public void attachImageToConversation(
 601            final Conversation conversation,
 602            final Uri uri,
 603            final String type,
 604            final String subject,
 605            final UiCallback<Message> callback) {
 606        final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type);
 607        final String compressPictures = getCompressPicturesPreference();
 608
 609        if ("never".equals(compressPictures)
 610                || ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))
 611                || (mimeType != null && mimeType.endsWith("/gif"))
 612                || getFileBackend().unusualBounds(uri) || "data".equals(uri.getScheme())) {
 613            Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": not compressing picture. sending as file");
 614            attachFileToConversation(conversation, uri, mimeType, subject, callback);
 615            return;
 616        }
 617        final Message message;
 618
 619        if (conversation.getReplyTo() == null) {
 620            message = new Message(conversation, "", conversation.getNextEncryption());
 621        } else {
 622            message = conversation.getReplyTo().reply();
 623            message.setEncryption(conversation.getNextEncryption());
 624        }
 625        if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 626            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 627        }
 628        if (subject != null && subject.length() > 0) message.setSubject(subject);
 629        message.setThread(conversation.getThread());
 630        if (!Message.configurePrivateFileMessage(message)) {
 631            message.setCounterpart(conversation.getNextCounterpart());
 632            message.setType(Message.TYPE_IMAGE);
 633        }
 634        Log.d(Config.LOGTAG, "attachImage: type=" + message.getType());
 635        FILE_ATTACHMENT_EXECUTOR.execute(() -> {
 636            try {
 637                getFileBackend().copyImageToPrivateStorage(message, uri);
 638            } catch (FileBackend.ImageCompressionException e) {
 639                Log.d(Config.LOGTAG, "unable to compress image. fall back to file transfer", e);
 640                attachFileToConversation(conversation, uri, mimeType, subject, callback);
 641                return;
 642            } catch (final FileBackend.FileCopyException e) {
 643                callback.error(e.getResId(), message);
 644                return;
 645            }
 646            if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 647                final PgpEngine pgpEngine = getPgpEngine();
 648                if (pgpEngine != null) {
 649                    pgpEngine.encrypt(message, callback);
 650                } else if (callback != null) {
 651                    callback.error(R.string.unable_to_connect_to_keychain, null);
 652                }
 653            } else {
 654                sendMessage(message, false, false, false, () -> callback.success(message));
 655            }
 656        });
 657    }
 658
 659    private File stickerDir() {
 660        SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
 661        final String dir = p.getString("sticker_directory", "Stickers");
 662        if (dir.startsWith("content://")) {
 663            Uri uri = Uri.parse(dir);
 664            uri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri));
 665            return new File(FileUtils.getPath(getBaseContext(), uri));
 666        } else {
 667            return new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/" + dir);
 668        }
 669    }
 670
 671    public void rescanStickers() {
 672        long msToRescan = (mLastStickerRescan + 600000L) - SystemClock.elapsedRealtime();
 673        if (msToRescan > 0) return;
 674        Log.d(Config.LOGTAG, "rescanStickers");
 675
 676        mLastStickerRescan = SystemClock.elapsedRealtime();
 677        mStickerScanExecutor.execute(() -> {
 678            Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
 679            try {
 680                for (File file : Files.fileTraverser().breadthFirst(stickerDir())) {
 681                    try {
 682                        if (file.isFile() && file.canRead()) {
 683                            DownloadableFile df = new DownloadableFile(file.getAbsolutePath());
 684                            Drawable icon = fileBackend.getThumbnail(df, getResources(), (int) (getResources().getDisplayMetrics().density * 288), false);
 685                            final String filename = Files.getNameWithoutExtension(df.getName());
 686                            Cid[] cids = fileBackend.calculateCids(new FileInputStream(df));
 687                            for (Cid cid : cids) {
 688                                saveCid(cid, file);
 689                            }
 690                            if (file.length() < 129000) {
 691                                emojiSearch.addEmoji(new EmojiSearch.CustomEmoji(filename, cids[0].toString(), icon, file.getParentFile().getName()));
 692                            }
 693                        }
 694                    } catch (final Exception e) {
 695                        Log.w(Config.LOGTAG, "rescanStickers: " + e);
 696                    }
 697                }
 698            } catch (final Exception e) {
 699                Log.w(Config.LOGTAG, "rescanStickers: " + e);
 700            }
 701        });
 702    }
 703
 704    protected void cleanupCache() {
 705        if (Build.VERSION.SDK_INT < 26) return; // Doesn't support file.toPath
 706        mStickerScanExecutor.execute(() -> {
 707            Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
 708            final var now = System.currentTimeMillis();
 709            try {
 710                for (File file : Files.fileTraverser().breadthFirst(getCacheDir())) {
 711                    if (file.isFile() && file.canRead() && file.canWrite()) {
 712                        final var attrs = java.nio.file.Files.readAttributes(file.toPath(), java.nio.file.attribute.BasicFileAttributes.class);
 713                        if ((now - attrs.lastAccessTime().toMillis()) > 1000L * 60 * 60 * 24 * 10) {
 714                            Log.d(Config.LOGTAG, "cleanupCache removing file not used recently: " + file);
 715                            file.delete();
 716                        }
 717                    }
 718                }
 719            } catch (final Exception e) {
 720                Log.w(Config.LOGTAG, "cleanupCache " + e);
 721            }
 722        });
 723    }
 724
 725    public EmojiSearch emojiSearch() {
 726        return emojiSearch;
 727    }
 728
 729    public Conversation find(Bookmark bookmark) {
 730        return find(bookmark.getAccount(), bookmark.getJid());
 731    }
 732
 733    public Conversation find(final Account account, final Jid jid) {
 734        return find(getConversations(), account, jid);
 735    }
 736
 737    public boolean isMuc(final Account account, final Jid jid) {
 738        final Conversation c = find(account, jid);
 739        return c != null && c.getMode() == Conversational.MODE_MULTI;
 740    }
 741
 742    public void search(
 743            final List<String> term,
 744            final String uuid,
 745            final OnSearchResultsAvailable onSearchResultsAvailable) {
 746        MessageSearchTask.search(this, term, uuid, onSearchResultsAvailable);
 747    }
 748
 749    @Override
 750    public int onStartCommand(final Intent intent, int flags, int startId) {
 751        final var nomedia = getBooleanPreference("nomedia", R.bool.default_nomedia);
 752        fileBackend.setupNomedia(nomedia);
 753        final String action = Strings.nullToEmpty(intent == null ? null : intent.getAction());
 754        final boolean needsForegroundService =
 755                intent != null
 756                        && intent.getBooleanExtra(
 757                                SystemEventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false);
 758        if (needsForegroundService) {
 759            Log.d(
 760                    Config.LOGTAG,
 761                    "toggle forced foreground service after receiving event (action="
 762                            + action
 763                            + ")");
 764            toggleForegroundService(true, action.equals(ACTION_STARTING_CALL));
 765        }
 766        final String uuid = intent == null ? null : intent.getStringExtra("uuid");
 767        switch (action) {
 768            case QuickConversationsService.SMS_RETRIEVED_ACTION:
 769                mQuickConversationsService.handleSmsReceived(intent);
 770                break;
 771            case ConnectivityManager.CONNECTIVITY_ACTION:
 772                if (hasInternetConnection()) {
 773                    if (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0) {
 774                        schedulePostConnectivityChange();
 775                    }
 776                    if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
 777                        resetAllAttemptCounts(true, false);
 778                    }
 779                    Resolver.clearCache();
 780                }
 781                break;
 782            case Intent.ACTION_SHUTDOWN:
 783                logoutAndSave(true);
 784                return START_NOT_STICKY;
 785            case ACTION_CLEAR_MESSAGE_NOTIFICATION:
 786                mNotificationExecutor.execute(
 787                        () -> {
 788                            try {
 789                                final Conversation c = findConversationByUuid(uuid);
 790                                if (c != null) {
 791                                    mNotificationService.clearMessages(c);
 792                                } else {
 793                                    mNotificationService.clearMessages();
 794                                }
 795                                restoredFromDatabaseLatch.await();
 796
 797                            } catch (InterruptedException e) {
 798                                Log.d(
 799                                        Config.LOGTAG,
 800                                        "unable to process clear message notification");
 801                            }
 802                        });
 803                break;
 804            case ACTION_CLEAR_MISSED_CALL_NOTIFICATION:
 805                mNotificationExecutor.execute(
 806                        () -> {
 807                            try {
 808                                final Conversation c = findConversationByUuid(uuid);
 809                                if (c != null) {
 810                                    mNotificationService.clearMissedCalls(c);
 811                                } else {
 812                                    mNotificationService.clearMissedCalls();
 813                                }
 814                                restoredFromDatabaseLatch.await();
 815
 816                            } catch (InterruptedException e) {
 817                                Log.d(
 818                                        Config.LOGTAG,
 819                                        "unable to process clear missed call notification");
 820                            }
 821                        });
 822                break;
 823            case ACTION_DISMISS_CALL:
 824                {
 825                    if (intent == null) {
 826                        break;
 827                    }
 828                    final String sessionId =
 829                            intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
 830                    Log.d(
 831                            Config.LOGTAG,
 832                            "received intent to dismiss call with session id " + sessionId);
 833                    mJingleConnectionManager.rejectRtpSession(sessionId);
 834                    break;
 835                }
 836            case TorServiceUtils.ACTION_STATUS:
 837                final String status =
 838                        intent == null ? null : intent.getStringExtra(TorServiceUtils.EXTRA_STATUS);
 839                // TODO port and host are in 'extras' - but this may not be a reliable source?
 840                if ("ON".equals(status)) {
 841                    handleOrbotStartedEvent();
 842                    return START_STICKY;
 843                }
 844                break;
 845            case ACTION_END_CALL:
 846                {
 847                    if (intent == null) {
 848                        break;
 849                    }
 850                    final String sessionId =
 851                            intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
 852                    Log.d(
 853                            Config.LOGTAG,
 854                            "received intent to end call with session id " + sessionId);
 855                    mJingleConnectionManager.endRtpSession(sessionId);
 856                }
 857                break;
 858            case ACTION_PROVISION_ACCOUNT:
 859                {
 860                    if (intent == null) {
 861                        break;
 862                    }
 863                    final String address = intent.getStringExtra("address");
 864                    final String password = intent.getStringExtra("password");
 865                    if (QuickConversationsService.isQuicksy()
 866                            || Strings.isNullOrEmpty(address)
 867                            || Strings.isNullOrEmpty(password)) {
 868                        break;
 869                    }
 870                    provisionAccount(address, password);
 871                    break;
 872                }
 873            case ACTION_DISMISS_ERROR_NOTIFICATIONS:
 874                dismissErrorNotifications();
 875                break;
 876            case ACTION_TRY_AGAIN:
 877                resetAllAttemptCounts(false, true);
 878                break;
 879            case ACTION_REPLY_TO_CONVERSATION:
 880                final Bundle remoteInput =
 881                        intent == null ? null : RemoteInput.getResultsFromIntent(intent);
 882                if (remoteInput == null) {
 883                    break;
 884                }
 885                final CharSequence body = remoteInput.getCharSequence("text_reply");
 886                final boolean dismissNotification =
 887                        intent.getBooleanExtra("dismiss_notification", false);
 888                final String lastMessageUuid = intent.getStringExtra("last_message_uuid");
 889                if (body == null || body.length() <= 0) {
 890                    break;
 891                }
 892                mNotificationExecutor.execute(
 893                        () -> {
 894                            try {
 895                                restoredFromDatabaseLatch.await();
 896                                final Conversation c = findConversationByUuid(uuid);
 897                                if (c != null) {
 898                                    directReply(
 899                                            c,
 900                                            body.toString(),
 901                                            lastMessageUuid,
 902                                            dismissNotification);
 903                                }
 904                            } catch (InterruptedException e) {
 905                                Log.d(Config.LOGTAG, "unable to process direct reply");
 906                            }
 907                        });
 908                break;
 909            case ACTION_MARK_AS_READ:
 910                mNotificationExecutor.execute(
 911                        () -> {
 912                            final Conversation c = findConversationByUuid(uuid);
 913                            if (c == null) {
 914                                Log.d(
 915                                        Config.LOGTAG,
 916                                        "received mark read intent for unknown conversation ("
 917                                                + uuid
 918                                                + ")");
 919                                return;
 920                            }
 921                            try {
 922                                restoredFromDatabaseLatch.await();
 923                                sendReadMarker(c, null);
 924                            } catch (InterruptedException e) {
 925                                Log.d(
 926                                        Config.LOGTAG,
 927                                        "unable to process notification read marker for"
 928                                                + " conversation "
 929                                                + c.getName());
 930                            }
 931                        });
 932                break;
 933            case ACTION_SNOOZE:
 934                mNotificationExecutor.execute(
 935                        () -> {
 936                            final Conversation c = findConversationByUuid(uuid);
 937                            if (c == null) {
 938                                Log.d(
 939                                        Config.LOGTAG,
 940                                        "received snooze intent for unknown conversation ("
 941                                                + uuid
 942                                                + ")");
 943                                return;
 944                            }
 945                            c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000);
 946                            mNotificationService.clearMessages(c);
 947                            updateConversation(c);
 948                        });
 949            case AudioManager.RINGER_MODE_CHANGED_ACTION:
 950            case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED:
 951                if (appSettings.isDndOnSilentMode() && appSettings.isAutomaticAvailability()) {
 952                    refreshAllPresences();
 953                }
 954                break;
 955            case Intent.ACTION_SCREEN_ON:
 956                deactivateGracePeriod();
 957            case Intent.ACTION_USER_PRESENT:
 958            case Intent.ACTION_SCREEN_OFF:
 959                if (appSettings.isAwayWhenScreenLocked() && appSettings.isAutomaticAvailability()) {
 960                    refreshAllPresences();
 961                }
 962                break;
 963            case ACTION_FCM_TOKEN_REFRESH:
 964                refreshAllFcmTokens();
 965                break;
 966            case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS:
 967                if (intent == null) {
 968                    break;
 969                }
 970                final String instance = intent.getStringExtra("instance");
 971                final String application = intent.getStringExtra("application");
 972                final Messenger messenger = intent.getParcelableExtra("messenger");
 973                final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger;
 974                if (messenger != null && application != null && instance != null) {
 975                    pushTargetMessenger =
 976                            new UnifiedPushBroker.PushTargetMessenger(
 977                                    new UnifiedPushDatabase.PushTarget(application, instance),
 978                                    messenger);
 979                    Log.d(Config.LOGTAG, "found push target messenger");
 980                } else {
 981                    pushTargetMessenger = null;
 982                }
 983                final Optional<UnifiedPushBroker.Transport> transport =
 984                        renewUnifiedPushEndpoints(pushTargetMessenger);
 985                if (instance != null && transport.isPresent()) {
 986                    unifiedPushBroker.rebroadcastEndpoint(messenger, instance, transport.get());
 987                }
 988                break;
 989            case ACTION_IDLE_PING:
 990                scheduleNextIdlePing();
 991                break;
 992            case ACTION_FCM_MESSAGE_RECEIVED:
 993                Log.d(Config.LOGTAG, "push message arrived in service. account");
 994                break;
 995            case ACTION_QUICK_LOG:
 996                final String message = intent == null ? null : intent.getStringExtra("message");
 997                if (message != null && Config.QUICK_LOG) {
 998                    quickLog(message);
 999                }
1000                break;
1001            case Intent.ACTION_SEND:
1002                final Uri uri = intent == null ? null : intent.getData();
1003                if (uri != null) {
1004                    Log.d(Config.LOGTAG, "received uri permission for " + uri);
1005                }
1006                return START_STICKY;
1007            case ACTION_TEMPORARILY_DISABLE:
1008                toggleSoftDisabled(true);
1009                if (checkListeners()) {
1010                    stopSelf();
1011                }
1012                return START_NOT_STICKY;
1013        }
1014        sendScheduledMessages();
1015        final var extras =  intent == null ? null : intent.getExtras();
1016        try {
1017            internalPingExecutor.execute(() -> manageAccountConnectionStates(action, extras));
1018        } catch (final RejectedExecutionException e) {
1019            Log.e(Config.LOGTAG, "can not schedule connection states manager");
1020        }
1021        if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) {
1022            expireOldMessages();
1023        }
1024        return START_STICKY;
1025    }
1026
1027    private void quickLog(final String message) {
1028        if (Strings.isNullOrEmpty(message)) {
1029            return;
1030        }
1031        final Account account = AccountUtils.getFirstEnabled(this);
1032        if (account == null) {
1033            return;
1034        }
1035        final Conversation conversation =
1036                findOrCreateConversation(account, Config.BUG_REPORTS, false, true);
1037        final Message report = new Message(conversation, message, Message.ENCRYPTION_NONE);
1038        report.setStatus(Message.STATUS_RECEIVED);
1039        conversation.add(report);
1040        databaseBackend.createMessage(report);
1041        updateConversationUi();
1042    }
1043
1044    public void manageAccountConnectionStatesInternal() {
1045        manageAccountConnectionStates(ACTION_INTERNAL_PING, null);
1046    }
1047
1048    private synchronized void manageAccountConnectionStates(
1049            final String action, final Bundle extras) {
1050        final String pushedAccountHash = extras == null ? null : extras.getString("account");
1051        final boolean interactive = java.util.Objects.equals(ACTION_TRY_AGAIN, action);
1052        WakeLockHelper.acquire(wakeLock);
1053        boolean pingNow =
1054                ConnectivityManager.CONNECTIVITY_ACTION.equals(action)
1055                        || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0
1056                                && ACTION_POST_CONNECTIVITY_CHANGE.equals(action));
1057        final HashSet<Account> pingCandidates = new HashSet<>();
1058        final String androidId = pushedAccountHash == null ? null : PhoneHelper.getAndroidId(this);
1059        for (final Account account : accounts) {
1060            final boolean pushWasMeantForThisAccount =
1061                    androidId != null
1062                            && CryptoHelper.getAccountFingerprint(account, androidId)
1063                                    .equals(pushedAccountHash);
1064            pingNow |=
1065                    processAccountState(
1066                            account,
1067                            interactive,
1068                            "ui".equals(action),
1069                            pushWasMeantForThisAccount,
1070                            pingCandidates);
1071        }
1072        if (pingNow) {
1073            for (final Account account : pingCandidates) {
1074                final var connection = account.getXmppConnection();
1075                final boolean lowTimeout = isInLowPingTimeoutMode(account);
1076                final var delta =
1077                        (SystemClock.elapsedRealtime() - connection.getLastPacketReceived())
1078                                / 1000L;
1079                connection.sendPing();
1080                Log.d(
1081                        Config.LOGTAG,
1082                        String.format(
1083                                "%s: send ping (action=%s,lowTimeout=%s,interval=%s)",
1084                                account.getJid().asBareJid(), action, lowTimeout, delta));
1085                scheduleWakeUpCall(
1086                        lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT,
1087                        account.getUuid().hashCode());
1088            }
1089        }
1090        long msToMucPing = (mLastMucPing + (Config.PING_MAX_INTERVAL * 2000L)) - SystemClock.elapsedRealtime();
1091        if (pingNow || ("ui".equals(action) && msToMucPing <= 0) || msToMucPing < -300000) {
1092            Log.d(Config.LOGTAG, "ping MUCs");
1093            mLastMucPing = SystemClock.elapsedRealtime();
1094            for (Conversation c : getConversations()) {
1095                if (c.getMode() == Conversation.MODE_MULTI && (c.getMucOptions().online() || c.getMucOptions().getError() == MucOptions.Error.SHUTDOWN)) {
1096                    c.getAccount().getXmppConnection().getManager(MultiUserChatManager.class).pingAndRejoin(c);
1097                }
1098            }
1099        }
1100        WakeLockHelper.release(wakeLock);
1101    }
1102
1103    private void sendScheduledMessages() {
1104        Log.d(Config.LOGTAG, "looking for and sending scheduled messages");
1105
1106        for (final var message : new ArrayList<>(mScheduledMessages.values())) {
1107            if (message.getTimeSent() > System.currentTimeMillis()) continue;
1108
1109            final var conversation = message.getConversation();
1110            final var account = conversation.getAccount();
1111            final boolean inProgressJoin =
1112                conversation instanceof Conversation ? conversation.getAccount().getXmppConnection()
1113                        .getManager(MultiUserChatManager.class)
1114                        .isJoinInProgress((Conversation) conversation) : false;
1115            if (conversation.getAccount() == account
1116                    && !inProgressJoin) {
1117                resendMessage(message, false);
1118            }
1119        }
1120    }
1121
1122    private void handleOrbotStartedEvent() {
1123        for (final Account account : accounts) {
1124            if (account.getStatus() == Account.State.TOR_NOT_AVAILABLE) {
1125                reconnectAccount(account, true, false);
1126            }
1127        }
1128    }
1129
1130    private boolean processAccountState(
1131            final Account account,
1132            final boolean interactive,
1133            final boolean isUiAction,
1134            final boolean isAccountPushed,
1135            final HashSet<Account> pingCandidates) {
1136        final var connection = account.getXmppConnection();
1137        if (!account.getStatus().isAttemptReconnect()) {
1138            return false;
1139        }
1140        final var requestCode = account.getUuid().hashCode();
1141        if (!hasInternetConnection()) {
1142            connection.setStatusAndTriggerProcessor(Account.State.NO_INTERNET);
1143        } else {
1144            if (account.getStatus() == Account.State.NO_INTERNET) {
1145                connection.setStatusAndTriggerProcessor(Account.State.OFFLINE);
1146            }
1147            if (account.getStatus() == Account.State.ONLINE) {
1148                synchronized (mLowPingTimeoutMode) {
1149                    long lastReceived = account.getXmppConnection().getLastPacketReceived();
1150                    long lastSent = account.getXmppConnection().getLastPingSent();
1151                    long pingInterval =
1152                            isUiAction
1153                                    ? Config.PING_MIN_INTERVAL * 1000
1154                                    : Config.PING_MAX_INTERVAL * 1000;
1155                    long msToNextPing =
1156                            (Math.max(lastReceived, lastSent) + pingInterval)
1157                                    - SystemClock.elapsedRealtime();
1158                    int pingTimeout =
1159                            mLowPingTimeoutMode.contains(account.getJid().asBareJid())
1160                                    ? Config.LOW_PING_TIMEOUT * 1000
1161                                    : Config.PING_TIMEOUT * 1000;
1162                    long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
1163                    if (lastSent > lastReceived) {
1164                        if (pingTimeoutIn < 0) {
1165                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping timeout");
1166                            this.reconnectAccount(account, true, interactive);
1167                        } else {
1168                            this.scheduleWakeUpCall(pingTimeoutIn, requestCode);
1169                        }
1170                    } else {
1171                        pingCandidates.add(account);
1172                        if (isAccountPushed) {
1173                            if (mLowPingTimeoutMode.add(account.getJid().asBareJid())) {
1174                                Log.d(
1175                                        Config.LOGTAG,
1176                                        account.getJid().asBareJid()
1177                                                + ": entering low ping timeout mode");
1178                            }
1179                            return true;
1180                        } else if (msToNextPing <= 0) {
1181                            return true;
1182                        } else {
1183                            this.scheduleWakeUpCall(msToNextPing, requestCode);
1184                            if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
1185                                Log.d(
1186                                        Config.LOGTAG,
1187                                        account.getJid().asBareJid()
1188                                                + ": leaving low ping timeout mode");
1189                            }
1190                        }
1191                    }
1192                }
1193            } else if (account.getStatus() == Account.State.OFFLINE) {
1194                reconnectAccount(account, true, interactive);
1195            } else if (account.getStatus() == Account.State.CONNECTING) {
1196                final var connectionDuration = connection.getConnectionDuration();
1197                final var discoDuration = connection.getDiscoDuration();
1198                final var connectionTimeout = Config.CONNECT_TIMEOUT * 1000L - connectionDuration;
1199                final var discoTimeout = Config.CONNECT_DISCO_TIMEOUT * 1000L - discoDuration;
1200                if (connectionTimeout < 0) {
1201                    connection.triggerConnectionTimeout();
1202                } else if (discoTimeout < 0) {
1203                    connection.sendDiscoTimeout();
1204                    scheduleWakeUpCall(discoTimeout, requestCode);
1205                } else {
1206                    scheduleWakeUpCall(Math.min(connectionTimeout, discoTimeout), requestCode);
1207                }
1208            } else {
1209                final boolean aggressive =
1210                        account.getStatus() == Account.State.SEE_OTHER_HOST
1211                                || hasJingleRtpConnection(account);
1212                if (connection.getTimeToNextAttempt(aggressive) <= 0) {
1213                    reconnectAccount(account, true, interactive);
1214                }
1215            }
1216        }
1217        return false;
1218    }
1219
1220    private void toggleSoftDisabled(final boolean softDisabled) {
1221        for (final Account account : this.accounts) {
1222            if (account.isEnabled()) {
1223                if (account.setOption(Account.OPTION_SOFT_DISABLED, softDisabled)) {
1224                    updateAccount(account);
1225                }
1226            }
1227        }
1228    }
1229
1230    public void fetchServiceOutageStatus(final Account account) {
1231        final var sosUrl = account.getKey(Account.KEY_SOS_URL);
1232        if (Strings.isNullOrEmpty(sosUrl)) {
1233            return;
1234        }
1235        final var url = HttpUrl.parse(sosUrl);
1236        if (url == null) {
1237            return;
1238        }
1239        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching service outage " + url);
1240        Futures.addCallback(
1241                ServiceOutageStatus.fetch(getApplicationContext(), url),
1242                new FutureCallback<>() {
1243                    @Override
1244                    public void onSuccess(final ServiceOutageStatus sos) {
1245                        Log.d(Config.LOGTAG, "fetched " + sos);
1246                        account.setServiceOutageStatus(sos);
1247                        updateAccountUi();
1248                    }
1249
1250                    @Override
1251                    public void onFailure(@NonNull Throwable throwable) {
1252                        Log.d(Config.LOGTAG, "error fetching sos", throwable);
1253                    }
1254                },
1255                MoreExecutors.directExecutor());
1256    }
1257
1258    public boolean processUnifiedPushMessage(
1259            final Account account, final Jid transport, final Push push) {
1260        return unifiedPushBroker.processPushMessage(account, transport, push);
1261    }
1262
1263    public void reinitializeMuclumbusService() {
1264        mChannelDiscoveryService.initializeMuclumbusService();
1265    }
1266
1267    public void discoverChannels(
1268            String query,
1269            ChannelDiscoveryService.Method method,
1270            Map<Jid, XmppConnection> mucServices,
1271            ChannelDiscoveryService.OnChannelSearchResultsFound onChannelSearchResultsFound) {
1272        mChannelDiscoveryService.discover(
1273                Strings.nullToEmpty(query).trim(), method, mucServices, onChannelSearchResultsFound);
1274    }
1275
1276    public boolean isDataSaverDisabled() {
1277        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
1278            return true;
1279        }
1280        final ConnectivityManager connectivityManager = getSystemService(ConnectivityManager.class);
1281        return !Compatibility.isActiveNetworkMetered(connectivityManager)
1282                || Compatibility.getRestrictBackgroundStatus(connectivityManager)
1283                        == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
1284    }
1285
1286    private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {
1287        final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid);
1288        Message message = new Message(conversation, body, conversation.getNextEncryption());
1289        if (inReplyTo != null) {
1290            if (Emoticons.isEmoji(body.replaceAll("\\s", ""))) {
1291                final var aggregated = inReplyTo.getAggregatedReactions();
1292                final ImmutableSet.Builder<String> reactionBuilder = new ImmutableSet.Builder<>();
1293                reactionBuilder.addAll(aggregated.ourReactions);
1294                reactionBuilder.add(body.replaceAll("\\s", ""));
1295                sendReactions(inReplyTo, reactionBuilder.build());
1296                return;
1297            } else {
1298                message = inReplyTo.reply();
1299            }
1300            message.clearFallbacks("urn:xmpp:reply:0");
1301            message.setBody(body);
1302            message.setEncryption(conversation.getNextEncryption());
1303        }
1304        if (inReplyTo != null && inReplyTo.isPrivateMessage()) {
1305            Message.configurePrivateMessage(message, inReplyTo.getCounterpart());
1306        }
1307        message.markUnread();
1308        if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1309            getPgpEngine()
1310                    .encrypt(
1311                            message,
1312                            new UiCallback<Message>() {
1313                                @Override
1314                                public void success(Message message) {
1315                                    if (dismissAfterReply) {
1316                                        markRead((Conversation) message.getConversation(), true);
1317                                    } else {
1318                                        mNotificationService.pushFromDirectReply(message);
1319                                    }
1320                                }
1321
1322                                @Override
1323                                public void error(int errorCode, Message object) {}
1324
1325                                @Override
1326                                public void userInputRequired(PendingIntent pi, Message object) {}
1327                            });
1328        } else {
1329            sendMessage(message);
1330            if (dismissAfterReply) {
1331                markRead(conversation, true);
1332            } else {
1333                mNotificationService.pushFromDirectReply(message);
1334            }
1335        }
1336    }
1337
1338    private String getCompressPicturesPreference() {
1339        return getPreferences()
1340                .getString(
1341                        "picture_compression",
1342                        getResources().getString(R.string.picture_compression));
1343    }
1344
1345    private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) {
1346        Log.d(Config.LOGTAG, "resetting all attempt counts");
1347        for (Account account : accounts) {
1348            if (account.hasErrorStatus() || reallyAll) {
1349                final XmppConnection connection = account.getXmppConnection();
1350                if (connection != null) {
1351                    connection.resetAttemptCount(retryImmediately);
1352                }
1353            }
1354            if (account.setShowErrorNotification(true)) {
1355                mDatabaseWriterExecutor.execute(() -> databaseBackend.updateAccount(account));
1356            }
1357        }
1358        mNotificationService.updateErrorNotification();
1359    }
1360
1361    private void dismissErrorNotifications() {
1362        for (final Account account : this.accounts) {
1363            if (account.hasErrorStatus()) {
1364                Log.d(
1365                        Config.LOGTAG,
1366                        account.getJid().asBareJid() + ": dismissing error notification");
1367                if (account.setShowErrorNotification(false)) {
1368                    mDatabaseWriterExecutor.execute(() -> databaseBackend.updateAccount(account));
1369                }
1370            }
1371        }
1372    }
1373
1374    private void expireOldMessages() {
1375        expireOldMessages(false);
1376    }
1377
1378    public void expireOldMessages(final boolean resetHasMessagesLeftOnServer) {
1379        mLastExpiryRun.set(SystemClock.elapsedRealtime());
1380        mDatabaseWriterExecutor.execute(
1381                () -> {
1382                    long timestamp = getAutomaticMessageDeletionDate();
1383                    if (timestamp > 0) {
1384                        databaseBackend.expireOldMessages(timestamp);
1385                        synchronized (XmppConnectionService.this.conversations) {
1386                            for (Conversation conversation :
1387                                    XmppConnectionService.this.conversations) {
1388                                conversation.expireOldMessages(timestamp);
1389                                if (resetHasMessagesLeftOnServer) {
1390                                    conversation.messagesLoaded.set(true);
1391                                    conversation.setHasMessagesLeftOnServer(true);
1392                                }
1393                            }
1394                        }
1395                        updateConversationUi();
1396                    }
1397                });
1398    }
1399
1400    public boolean hasInternetConnection() {
1401        final ConnectivityManager cm =
1402                ContextCompat.getSystemService(this, ConnectivityManager.class);
1403        if (cm == null) {
1404            return true; // if internet connection can not be checked it is probably best to just
1405            // try
1406        }
1407        try {
1408            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
1409                final Network activeNetwork = cm.getActiveNetwork();
1410                final NetworkCapabilities capabilities =
1411                        activeNetwork == null ? null : cm.getNetworkCapabilities(activeNetwork);
1412                return capabilities != null
1413                        && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
1414            } else {
1415                final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
1416                return networkInfo != null
1417                        && (networkInfo.isConnected()
1418                                || networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET);
1419            }
1420        } catch (final RuntimeException e) {
1421            Log.d(Config.LOGTAG, "unable to check for internet connection", e);
1422            return true; // if internet connection can not be checked it is probably best to just
1423            // try
1424        }
1425    }
1426
1427    @SuppressLint("TrulyRandom")
1428    @Override
1429    public void onCreate() {
1430        com.cheogram.android.AndroidLoggingHandler.reset(new com.cheogram.android.AndroidLoggingHandler());
1431        java.util.logging.Logger.getLogger("").setLevel(java.util.logging.Level.FINEST);
1432        LibIdnXmppStringprep.setup();
1433        emojiSearch = new EmojiSearch(this);
1434        setTheme(R.style.Theme_Conversations3);
1435        ThemeHelper.applyCustomColors(this);
1436        if (Compatibility.twentySix()) {
1437            mNotificationService.initializeChannels();
1438        }
1439        mChannelDiscoveryService.initializeMuclumbusService();
1440        mForceDuringOnCreate.set(Compatibility.twentySix());
1441        toggleForegroundService();
1442        this.destroyed = false;
1443        OmemoSetting.load(this);
1444        try {
1445            Security.insertProviderAt(Conscrypt.newProvider(), 1);
1446        } catch (Throwable throwable) {
1447            Log.e(Config.LOGTAG, "unable to initialize security provider", throwable);
1448        }
1449        Resolver.init(this);
1450        updateMemorizingTrustManager();
1451        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
1452        final int cacheSize = maxMemory / 8;
1453        this.mDrawableCache = new LruCache<String, Drawable>(cacheSize) {
1454            @Override
1455            protected int sizeOf(final String key, final Drawable drawable) {
1456                if (drawable instanceof BitmapDrawable) {
1457                    Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
1458                    if (bitmap == null) return 1024;
1459
1460                    return bitmap.getByteCount() / 1024;
1461                } else if (drawable instanceof AvatarService.TextDrawable) {
1462                    return 50;
1463                } else {
1464                    return drawable.getIntrinsicWidth() * drawable.getIntrinsicHeight() * 40 / 1024;
1465                }
1466            }
1467        };
1468        if (mLastActivity == 0) {
1469            mLastActivity =
1470                    getPreferences().getLong(SETTING_LAST_ACTIVITY_TS, System.currentTimeMillis());
1471        }
1472
1473        Log.d(Config.LOGTAG, "initializing database...");
1474        this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
1475        Log.d(Config.LOGTAG, "restoring accounts...");
1476        this.accounts = databaseBackend.getAccounts();
1477        for (final var account : this.accounts) {
1478            final int color = getPreferences().getInt("account_color:" + account.getUuid(), 0);
1479            if (color != 0) account.setColor(color);
1480            account.setXmppConnection(createConnection(account));
1481        }
1482        final SharedPreferences.Editor editor = getPreferences().edit();
1483        final boolean hasEnabledAccounts = hasEnabledAccounts();
1484        editor.putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
1485        editor.apply();
1486        toggleSetProfilePictureActivity(hasEnabledAccounts);
1487        reconfigurePushDistributor();
1488
1489        if (CallIntegration.hasSystemFeature(this)) {
1490            CallIntegrationConnectionService.togglePhoneAccountsAsync(this, this.accounts);
1491        }
1492
1493        restoreFromDatabase();
1494
1495        if (QuickConversationsService.isContactListIntegration(this)
1496                && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
1497                        == PackageManager.PERMISSION_GRANTED) {
1498            startContactObserver();
1499        }
1500        FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath);
1501        if (Compatibility.hasStoragePermission(this)) {
1502            Log.d(Config.LOGTAG, "starting file observer");
1503            FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::startWatching);
1504            FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
1505        }
1506        if (Config.supportOpenPgp()) {
1507            this.pgpServiceConnection =
1508                    new OpenPgpServiceConnection(
1509                            this,
1510                            "org.sufficientlysecure.keychain",
1511                            new OpenPgpServiceConnection.OnBound() {
1512                                @Override
1513                                public void onBound(final IOpenPgpService2 service) {
1514                                    for (Account account : accounts) {
1515                                        final PgpDecryptionService pgp =
1516                                                account.getPgpDecryptionService();
1517                                        if (pgp != null) {
1518                                            pgp.continueDecryption(true);
1519                                        }
1520                                    }
1521                                }
1522
1523                                @Override
1524                                public void onError(final Exception exception) {
1525                                    Log.e(
1526                                            Config.LOGTAG,
1527                                            "could not bind to OpenKeyChain",
1528                                            exception);
1529                                }
1530                            });
1531            this.pgpServiceConnection.bindToService();
1532        }
1533
1534        final PowerManager powerManager = getSystemService(PowerManager.class);
1535        if (powerManager != null) {
1536            this.wakeLock =
1537                    powerManager.newWakeLock(
1538                            PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service");
1539        }
1540
1541        toggleForegroundService();
1542        updateUnreadCountBadge();
1543        toggleScreenEventReceiver();
1544        final IntentFilter systemBroadcastFilter = new IntentFilter();
1545        scheduleNextIdlePing();
1546        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1547            systemBroadcastFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
1548        }
1549        systemBroadcastFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
1550        ContextCompat.registerReceiver(
1551                this,
1552                this.mInternalEventReceiver,
1553                systemBroadcastFilter,
1554                ContextCompat.RECEIVER_NOT_EXPORTED);
1555        final IntentFilter exportedBroadcastFilter = new IntentFilter();
1556        exportedBroadcastFilter.addAction(TorServiceUtils.ACTION_STATUS);
1557        ContextCompat.registerReceiver(
1558                this,
1559                this.mInternalRestrictedEventReceiver,
1560                exportedBroadcastFilter,
1561                ContextCompat.RECEIVER_EXPORTED);
1562        mForceDuringOnCreate.set(false);
1563        toggleForegroundService();
1564        rescanStickers();
1565        cleanupCache();
1566        internalPingExecutor.scheduleWithFixedDelay(
1567                this::manageAccountConnectionStatesInternal, 10, 10, TimeUnit.SECONDS);
1568        final SharedPreferences sharedPreferences =
1569                androidx.preference.PreferenceManager.getDefaultSharedPreferences(this);
1570        sharedPreferences.registerOnSharedPreferenceChangeListener(
1571                new SharedPreferences.OnSharedPreferenceChangeListener() {
1572                    @Override
1573                    public void onSharedPreferenceChanged(
1574                            SharedPreferences sharedPreferences, @Nullable String key) {
1575                        Log.d(Config.LOGTAG, "preference '" + key + "' has changed");
1576                        if (AppSettings.KEEP_FOREGROUND_SERVICE.equals(key)) {
1577                            toggleForegroundService();
1578                        }
1579                    }
1580                });
1581    }
1582
1583    private void checkForDeletedFiles() {
1584        if (destroyed) {
1585            Log.d(
1586                    Config.LOGTAG,
1587                    "Do not check for deleted files because service has been destroyed");
1588            return;
1589        }
1590        final long start = SystemClock.elapsedRealtime();
1591        final List<DatabaseBackend.FilePathInfo> relativeFilePaths =
1592                databaseBackend.getFilePathInfo();
1593        final List<DatabaseBackend.FilePathInfo> changed = new ArrayList<>();
1594        for (final DatabaseBackend.FilePathInfo filePath : relativeFilePaths) {
1595            if (destroyed) {
1596                Log.d(
1597                        Config.LOGTAG,
1598                        "Stop checking for deleted files because service has been destroyed");
1599                return;
1600            }
1601            final File file = fileBackend.getFileForPath(filePath.path);
1602            if (filePath.setDeleted(!file.exists())) {
1603                changed.add(filePath);
1604            }
1605        }
1606        final long duration = SystemClock.elapsedRealtime() - start;
1607        Log.d(
1608                Config.LOGTAG,
1609                "found "
1610                        + changed.size()
1611                        + " changed files on start up. total="
1612                        + relativeFilePaths.size()
1613                        + ". ("
1614                        + duration
1615                        + "ms)");
1616        if (changed.size() > 0) {
1617            databaseBackend.markFilesAsChanged(changed);
1618            markChangedFiles(changed);
1619        }
1620    }
1621
1622    public void startContactObserver() {
1623        getContentResolver()
1624                .registerContentObserver(
1625                        ContactsContract.Contacts.CONTENT_URI,
1626                        true,
1627                        new ContentObserver(null) {
1628                            @Override
1629                            public void onChange(boolean selfChange) {
1630                                super.onChange(selfChange);
1631                                if (restoredFromDatabaseLatch.getCount() == 0) {
1632                                    loadPhoneContacts();
1633                                }
1634                            }
1635                        });
1636    }
1637
1638    @Override
1639    public void onTrimMemory(int level) {
1640        super.onTrimMemory(level);
1641        if (level >= TRIM_MEMORY_COMPLETE) {
1642            Log.d(Config.LOGTAG, "clear cache due to low memory");
1643            getDrawableCache().evictAll();
1644        }
1645    }
1646
1647    @Override
1648    public void onDestroy() {
1649        try {
1650            unregisterReceiver(this.mInternalEventReceiver);
1651            unregisterReceiver(this.mInternalRestrictedEventReceiver);
1652            unregisterReceiver(this.mInternalScreenEventReceiver);
1653        } catch (final IllegalArgumentException e) {
1654            // ignored
1655        }
1656        destroyed = false;
1657        fileObserver.stopWatching();
1658        internalPingExecutor.shutdown();
1659        super.onDestroy();
1660    }
1661
1662    public void restartFileObserver() {
1663        Log.d(Config.LOGTAG, "restarting file observer");
1664        FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::restartWatching);
1665        FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
1666    }
1667
1668    public void toggleScreenEventReceiver() {
1669        if (appSettings.isAwayWhenScreenLocked() && appSettings.isAutomaticAvailability()) {
1670            final IntentFilter filter = new IntentFilter();
1671            filter.addAction(Intent.ACTION_SCREEN_ON);
1672            filter.addAction(Intent.ACTION_SCREEN_OFF);
1673            filter.addAction(Intent.ACTION_USER_PRESENT);
1674            registerReceiver(this.mInternalScreenEventReceiver, filter);
1675        } else {
1676            try {
1677                unregisterReceiver(this.mInternalScreenEventReceiver);
1678            } catch (IllegalArgumentException e) {
1679                // ignored
1680            }
1681        }
1682    }
1683
1684    public void toggleForegroundService() {
1685        toggleForegroundService(false, false);
1686    }
1687
1688    public void setOngoingCall(
1689            AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
1690        ongoingCall.set(new OngoingCall(id, media, reconnecting));
1691        toggleForegroundService(false, true);
1692    }
1693
1694    public void removeOngoingCall() {
1695        ongoingCall.set(null);
1696        toggleForegroundService(false, false);
1697    }
1698
1699    private void toggleForegroundService(boolean force, boolean needMic) {
1700        final boolean status;
1701        final OngoingCall ongoing = ongoingCall.get();
1702        final boolean ongoingVideoTranscoding = mOngoingVideoTranscoding.get();
1703        final int id;
1704        if (force
1705                || mForceDuringOnCreate.get()
1706                || ongoingVideoTranscoding
1707                || ongoing != null
1708                || (appSettings.isKeepForegroundService() && hasEnabledAccounts())) {
1709            final Notification notification;
1710            if (ongoing != null && !diallerIntegrationActive.get()) {
1711                notification = this.mNotificationService.getOngoingCallNotification(ongoing);
1712                id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
1713                startForegroundOrCatch(id, notification, true);
1714            } else if (ongoingVideoTranscoding) {
1715                notification = this.mNotificationService.getIndeterminateVideoTranscoding();
1716                id = NotificationService.ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID;
1717                startForegroundOrCatch(id, notification, false);
1718            } else {
1719                notification = this.mNotificationService.createForegroundNotification();
1720                id = NotificationService.FOREGROUND_NOTIFICATION_ID;
1721                startForegroundOrCatch(id, notification, needMic || ongoing != null || diallerIntegrationActive.get());
1722            }
1723            mNotificationService.notify(id, notification);
1724            status = true;
1725        } else {
1726            id = 0;
1727            stopForeground(true);
1728            status = false;
1729        }
1730
1731        for (final int toBeRemoved :
1732                Collections2.filter(
1733                        Arrays.asList(
1734                                NotificationService.FOREGROUND_NOTIFICATION_ID,
1735                                NotificationService.ONGOING_CALL_NOTIFICATION_ID,
1736                                NotificationService.ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID),
1737                        i -> i != id)) {
1738            mNotificationService.cancel(toBeRemoved);
1739        }
1740        Log.d(
1741                Config.LOGTAG,
1742                "ForegroundService: " + (status ? "on" : "off") + ", notification: " + id);
1743    }
1744
1745    private void startForegroundOrCatch(
1746            final int id, final Notification notification, final boolean requireMicrophone) {
1747        try {
1748            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
1749                final int foregroundServiceType;
1750                if (requireMicrophone
1751                        && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
1752                                == PackageManager.PERMISSION_GRANTED) {
1753                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
1754                    Log.d(Config.LOGTAG, "defaulting to microphone foreground service type");
1755                } else if (getSystemService(PowerManager.class)
1756                        .isIgnoringBatteryOptimizations(getPackageName())) {
1757                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED;
1758                } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
1759                        == PackageManager.PERMISSION_GRANTED) {
1760                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
1761                } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
1762                        == PackageManager.PERMISSION_GRANTED) {
1763                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
1764                } else {
1765                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
1766                    Log.w(Config.LOGTAG, "falling back to special use foreground service type");
1767                }
1768
1769                startForeground(id, notification, foregroundServiceType);
1770            } else {
1771                startForeground(id, notification);
1772            }
1773        } catch (final IllegalStateException | SecurityException e) {
1774            Log.e(Config.LOGTAG, "Could not start foreground service", e);
1775        }
1776    }
1777
1778    public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() {
1779        return !mOngoingVideoTranscoding.get()
1780                && ongoingCall.get() == null
1781                && appSettings.isKeepForegroundService()
1782                && hasEnabledAccounts();
1783    }
1784
1785    @Override
1786    public void onTaskRemoved(final Intent rootIntent) {
1787        super.onTaskRemoved(rootIntent);
1788        if ((appSettings.isKeepForegroundService() && hasEnabledAccounts())
1789                || mOngoingVideoTranscoding.get()
1790                || ongoingCall.get() != null) {
1791            Log.d(Config.LOGTAG, "ignoring onTaskRemoved because foreground service is activated");
1792        } else {
1793            this.logoutAndSave(false);
1794        }
1795    }
1796
1797    private void logoutAndSave(boolean stop) {
1798        int activeAccounts = 0;
1799        for (final Account account : accounts) {
1800            if (account.isConnectionEnabled()) {
1801                account.getXmppConnection().getManager(RosterManager.class).writeToDatabase();
1802                activeAccounts++;
1803            }
1804            if (account.getXmppConnection() != null) {
1805                new Thread(() -> disconnect(account, false)).start();
1806            }
1807        }
1808        if (stop || activeAccounts == 0) {
1809            Log.d(Config.LOGTAG, "good bye");
1810            stopSelf();
1811        }
1812    }
1813
1814    private void schedulePostConnectivityChange() {
1815        final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
1816        if (alarmManager == null) {
1817            return;
1818        }
1819        final long triggerAtMillis =
1820                SystemClock.elapsedRealtime()
1821                        + (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL * 1000);
1822        final Intent intent = new Intent(this, SystemEventReceiver.class);
1823        intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE);
1824        try {
1825            final PendingIntent pendingIntent =
1826                    PendingIntent.getBroadcast(
1827                            this,
1828                            1,
1829                            intent,
1830                            s()
1831                                    ? PendingIntent.FLAG_IMMUTABLE
1832                                            | PendingIntent.FLAG_UPDATE_CURRENT
1833                                    : PendingIntent.FLAG_UPDATE_CURRENT);
1834            alarmManager.setAndAllowWhileIdle(
1835                    AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
1836        } catch (RuntimeException e) {
1837            Log.e(Config.LOGTAG, "unable to schedule alarm for post connectivity change", e);
1838        }
1839    }
1840
1841    public void scheduleWakeUpCall(final int seconds, final int requestCode) {
1842        scheduleWakeUpCall((seconds < 0 ? 1 : seconds + 1) * 1000L, requestCode);
1843    }
1844
1845    public void scheduleWakeUpCall(final long milliSeconds, final int requestCode) {
1846        final var timeToWake = SystemClock.elapsedRealtime() + milliSeconds;
1847        final var alarmManager = getSystemService(AlarmManager.class);
1848        final Intent intent = new Intent(this, SystemEventReceiver.class);
1849        intent.setAction(ACTION_PING);
1850        try {
1851            final PendingIntent pendingIntent =
1852                    PendingIntent.getBroadcast(
1853                            this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE);
1854            alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
1855        } catch (final RuntimeException e) {
1856            Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e);
1857        }
1858    }
1859
1860    private void scheduleNextIdlePing() {
1861        long timeUntilWake = Config.IDLE_PING_INTERVAL * 1000;
1862        final var now = System.currentTimeMillis();
1863        for (final var message : mScheduledMessages.values()) {
1864            if (message.getTimeSent() <= now) continue; // Just in case
1865            if (message.getTimeSent() - now < timeUntilWake) timeUntilWake = message.getTimeSent() - now;
1866        }
1867        final var timeToWake = SystemClock.elapsedRealtime() + timeUntilWake;
1868        final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
1869        if (alarmManager == null) {
1870            Log.d(Config.LOGTAG, "no alarm manager?");
1871            return;
1872        }
1873        final Intent intent = new Intent(this, SystemEventReceiver.class);
1874        intent.setAction(ACTION_IDLE_PING);
1875        try {
1876            final PendingIntent pendingIntent =
1877                    PendingIntent.getBroadcast(
1878                            this,
1879                            0,
1880                            intent,
1881                            s()
1882                                    ? PendingIntent.FLAG_IMMUTABLE
1883                                            | PendingIntent.FLAG_UPDATE_CURRENT
1884                                    : PendingIntent.FLAG_UPDATE_CURRENT);
1885            alarmManager.setAndAllowWhileIdle(
1886                    AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
1887        } catch (RuntimeException e) {
1888            Log.d(Config.LOGTAG, "unable to schedule alarm for idle ping", e);
1889        }
1890    }
1891
1892    public XmppConnection createConnection(final Account account) {
1893        final XmppConnection connection = new XmppConnection(account, this);
1894        connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket));
1895        connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
1896        return connection;
1897    }
1898
1899    public void sendChatState(final Conversation conversation) {
1900        if (appSettings.isSendChatStates()) {
1901            final var packet = mMessageGenerator.generateChatState(conversation);
1902            sendMessagePacket(conversation.getAccount(), packet);
1903        }
1904    }
1905
1906    private void sendFileMessage(
1907            final Message message, final boolean delay, final boolean forceP2P, final Runnable cb) {
1908        final var account = message.getConversation().getAccount();
1909        Log.d(
1910                Config.LOGTAG,
1911                account.getJid().asBareJid() + ": send file message. forceP2P=" + forceP2P);
1912        if ((account.httpUploadAvailable(fileBackend.getFile(message, false).getSize())
1913                        || message.getConversation().getMode() == Conversation.MODE_MULTI)
1914                && !forceP2P) {
1915            mHttpConnectionManager.createNewUploadConnection(message, delay, cb);
1916        } else {
1917            mJingleConnectionManager.startJingleFileTransfer(message);
1918            if (cb != null) cb.run();
1919        }
1920    }
1921
1922    public void sendMessage(final Message message) {
1923        sendMessage(message, false, false, false, false, null);
1924    }
1925
1926    public void sendMessage(final Message message, final Runnable cb) {
1927        sendMessage(message, false, false, false, false, cb);
1928    }
1929
1930    private void sendMessage(final Message message, final boolean resend, final boolean previewedLinks, final boolean delay, final Runnable cb) {
1931        sendMessage(message, resend, previewedLinks, delay, false, cb);
1932    }
1933
1934    private void sendMessage(
1935            final Message message,
1936            final boolean resend,
1937            final boolean previewedLinks,
1938            final boolean delay,
1939            final boolean forceP2P,
1940            final Runnable cb) {
1941        final Account account = message.getConversation().getAccount();
1942        if (account.setShowErrorNotification(true)) {
1943            databaseBackend.updateAccount(account);
1944            mNotificationService.updateErrorNotification();
1945        }
1946        final Conversation conversation = (Conversation) message.getConversation();
1947        account.deactivateGracePeriod();
1948
1949        if (QuickConversationsService.isQuicksy()
1950                && conversation.getMode() == Conversation.MODE_SINGLE) {
1951            final Contact contact = conversation.getContact();
1952            if (!contact.showInRoster() && contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) {
1953                Log.d(
1954                        Config.LOGTAG,
1955                        account.getJid().asBareJid()
1956                                + ": adding "
1957                                + contact.getJid()
1958                                + " on sending message");
1959                createContact(contact);
1960            }
1961        }
1962
1963        im.conversations.android.xmpp.model.stanza.Message packet = null;
1964        final boolean addToConversation = !message.edited() && message.getRawBody() != null;
1965        boolean saveInDb = addToConversation;
1966        message.setStatus(Message.STATUS_WAITING);
1967
1968        if (message.getEncryption() != Message.ENCRYPTION_NONE
1969                && conversation.getMode() == Conversation.MODE_MULTI
1970                && conversation.isPrivateAndNonAnonymous()) {
1971            if (conversation.setAttribute(
1972                    Conversation.ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, true)) {
1973                databaseBackend.updateConversation(conversation);
1974            }
1975        }
1976
1977        final boolean inProgressJoin =
1978                account.getXmppConnection()
1979                        .getManager(MultiUserChatManager.class)
1980                        .isJoinInProgress(conversation);
1981
1982        if (message.getCounterpart() == null && !message.isPrivateMessage()) {
1983            message.setCounterpart(message.getConversation().getJid().asBareJid());
1984        }
1985
1986        boolean waitForPreview = false;
1987        if (getPreferences().getBoolean("send_link_previews", true) && !previewedLinks && !message.needsUploading() && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
1988            message.clearLinkDescriptions();
1989            final List<URI> links = message.getLinks();
1990            if (!links.isEmpty()) {
1991                waitForPreview = true;
1992                if (account.isOnlineAndConnected()) {
1993                    FILE_ATTACHMENT_EXECUTOR.execute(() -> {
1994                        for (URI link : links) {
1995                            if ("https".equals(link.getScheme())) {
1996                                try {
1997                                    HttpUrl url = HttpUrl.parse(link.toString());
1998                                    OkHttpClient http = getHttpConnectionManager().buildHttpClient(url, account, 5, false);
1999                                    final var request = new okhttp3.Request.Builder().url(url).head().build();
2000                                    okhttp3.Response response = null;
2001                                    if ("www.amazon.com".equals(link.getHost()) || "www.amazon.ca".equals(link.getHost())) {
2002                                        // Amazon blocks HEAD
2003                                        response = new okhttp3.Response.Builder().request(request).protocol(okhttp3.Protocol.HTTP_1_1).code(200).message("OK").addHeader("Content-Type", "text/html").build();
2004                                    } else {
2005                                        response = http.newCall(request).execute();
2006                                    }
2007                                    final String mimeType = response.header("Content-Type") == null ? "" : response.header("Content-Type");
2008                                    final boolean image = mimeType.startsWith("image/");
2009                                    final boolean audio = mimeType.startsWith("audio/");
2010                                    final boolean video = mimeType.startsWith("video/");
2011                                    final boolean pdf = mimeType.equals("application/pdf");
2012                                    final boolean html = mimeType.startsWith("text/html") || mimeType.startsWith("application/xhtml+xml");
2013                                    if (response.isSuccessful() && (image || audio || video || pdf)) {
2014                                        Message.FileParams params = message.getFileParams();
2015                                        params.url = url.toString();
2016                                        if (response.header("Content-Length") != null) params.size = Long.parseLong(response.header("Content-Length"), 10);
2017                                        if (!Message.configurePrivateFileMessage(message)) {
2018                                            message.setType(image ? Message.TYPE_IMAGE : Message.TYPE_FILE);
2019                                        }
2020                                        params.setName(HttpConnectionManager.extractFilenameFromResponse(response));
2021
2022                                        if (link.toString().equals(message.getRawBody())) {
2023                                            Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
2024                                            fallback.addChild("body", "urn:xmpp:fallback:0");
2025                                            message.addPayload(fallback);
2026                                        } else if (message.getRawBody().indexOf(link.toString()) >= 0) {
2027                                            // Part of the real body, not just a fallback
2028                                            Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
2029                                            fallback.addChild("body", "urn:xmpp:fallback:0")
2030                                                .setAttribute("start", "0")
2031                                                .setAttribute("end", "0");
2032                                            message.addPayload(fallback);
2033                                        }
2034
2035                                        final int encryption = message.getEncryption();
2036                                        getHttpConnectionManager().createNewDownloadConnection(message, false, (file) -> {
2037                                            message.setEncryption(encryption);
2038                                            synchronized (message.getConversation()) {
2039                                                if (message.getStatus() == Message.STATUS_WAITING) sendMessage(message, true, true, false, cb);
2040                                            }
2041                                        });
2042                                        return;
2043                                    } else if (response.isSuccessful() && html) {
2044                                        Semaphore waiter = new Semaphore(0);
2045                                        OpenGraphParser.Builder openGraphBuilder = new OpenGraphParser.Builder(new OpenGraphCallback() {
2046                                            @Override
2047                                            public void onPostResponse(OpenGraphResult result) {
2048                                                Element rdf = new Element("Description", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
2049                                                rdf.setAttribute("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
2050                                                rdf.setAttribute("rdf:about", link.toString());
2051                                                if (result.getTitle() != null && !"".equals(result.getTitle())) {
2052                                                    rdf.addChild("title", "https://ogp.me/ns#").setContent(result.getTitle());
2053                                                }
2054                                                if (result.getDescription() != null && !"".equals(result.getDescription())) {
2055                                                    rdf.addChild("description", "https://ogp.me/ns#").setContent(result.getDescription());
2056                                                }
2057                                                if (result.getUrl() != null) {
2058                                                    rdf.addChild("url", "https://ogp.me/ns#").setContent(result.getUrl());
2059                                                }
2060                                                if (result.getImage() != null) {
2061                                                    rdf.addChild("image", "https://ogp.me/ns#").setContent(result.getImage());
2062                                                }
2063                                                if (result.getType() != null) {
2064                                                    rdf.addChild("type", "https://ogp.me/ns#").setContent(result.getType());
2065                                                }
2066                                                if (result.getSiteName() != null) {
2067                                                    rdf.addChild("site_name", "https://ogp.me/ns#").setContent(result.getSiteName());
2068                                                }
2069                                                if (result.getVideo() != null) {
2070                                                    rdf.addChild("video", "https://ogp.me/ns#").setContent(result.getVideo());
2071                                                }
2072                                                message.addPayload(rdf);
2073                                                waiter.release();
2074                                            }
2075
2076                                            public void onError(String error) {
2077                                                waiter.release();
2078                                            }
2079                                        })
2080                                            .showNullOnEmpty(true)
2081                                            .maxBodySize(90000)
2082                                            .timeout(5000);
2083                                        if (useTorToConnect()) {
2084                                            openGraphBuilder = openGraphBuilder.jsoupProxy(new JsoupProxy("127.0.0.1", 8118));
2085                                        }
2086                                        openGraphBuilder.build().parse(link.toString());
2087                                        waiter.tryAcquire(10L, TimeUnit.SECONDS);
2088                                    }
2089                                } catch (final IOException | InterruptedException e) {  }
2090                            }
2091                        }
2092                        synchronized (message.getConversation()) {
2093                            if (message.getStatus() == Message.STATUS_WAITING) sendMessage(message, true, true, false, cb);
2094                        }
2095                    });
2096                }
2097            }
2098        }
2099
2100        boolean passedCbOn = false;
2101        if (account.isOnlineAndConnected() && !inProgressJoin && !waitForPreview && message.getTimeSent() <= System.currentTimeMillis()) {
2102            switch (message.getEncryption()) {
2103                case Message.ENCRYPTION_NONE:
2104                    if (message.needsUploading()) {
2105                        if (account.httpUploadAvailable(
2106                                        fileBackend.getFile(message, false).getSize())
2107                                || conversation.getMode() == Conversation.MODE_MULTI
2108                                || message.fixCounterpart()) {
2109                            this.sendFileMessage(message, delay, forceP2P, cb);
2110                            passedCbOn = true;
2111                        } else {
2112                            break;
2113                        }
2114                    } else {
2115                        packet = mMessageGenerator.generateChat(message);
2116                    }
2117                    break;
2118                case Message.ENCRYPTION_PGP:
2119                case Message.ENCRYPTION_DECRYPTED:
2120                    if (message.needsUploading()) {
2121                        if (account.httpUploadAvailable(
2122                                        fileBackend.getFile(message, false).getSize())
2123                                || conversation.getMode() == Conversation.MODE_MULTI
2124                                || message.fixCounterpart()) {
2125                            this.sendFileMessage(message, delay, forceP2P, cb);
2126                            passedCbOn = true;
2127                        } else {
2128                            break;
2129                        }
2130                    } else {
2131                        packet = mMessageGenerator.generatePgpChat(message);
2132                    }
2133                    break;
2134                case Message.ENCRYPTION_AXOLOTL:
2135                    message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
2136                    if (message.needsUploading()) {
2137                        if (account.httpUploadAvailable(
2138                                        fileBackend.getFile(message, false).getSize())
2139                                || conversation.getMode() == Conversation.MODE_MULTI
2140                                || message.fixCounterpart()) {
2141                            this.sendFileMessage(message, delay, forceP2P, cb);
2142                            passedCbOn = true;
2143                        } else {
2144                            break;
2145                        }
2146                    } else {
2147                        XmppAxolotlMessage axolotlMessage =
2148                                account.getAxolotlService().fetchAxolotlMessageFromCache(message);
2149                        if (axolotlMessage == null) {
2150                            account.getAxolotlService().preparePayloadMessage(message, delay);
2151                        } else {
2152                            packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
2153                        }
2154                    }
2155                    break;
2156            }
2157            if (packet != null) {
2158                if (account.getXmppConnection().getFeatures().sm()
2159                        || (conversation.getMode() == Conversation.MODE_MULTI
2160                                && message.getCounterpart().isBareJid())) {
2161                    message.setStatus(Message.STATUS_UNSEND);
2162                } else {
2163                    message.setStatus(Message.STATUS_SEND);
2164                }
2165            }
2166        } else {
2167            switch (message.getEncryption()) {
2168                case Message.ENCRYPTION_DECRYPTED:
2169                    if (!message.needsUploading()) {
2170                        String pgpBody = message.getEncryptedBody();
2171                        String decryptedBody = message.getBody();
2172                        message.setBody(pgpBody); // TODO might throw NPE
2173                        message.setEncryption(Message.ENCRYPTION_PGP);
2174                        if (message.edited()) {
2175                            message.setBody(decryptedBody);
2176                            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
2177                            if (!databaseBackend.updateMessage(message, message.getEditedId())) {
2178                                Log.e(Config.LOGTAG, "error updated message in DB after edit");
2179                            }
2180                            updateConversationUi();
2181                            if (!waitForPreview && cb != null) cb.run();
2182                            return;
2183                        } else {
2184                            databaseBackend.createMessage(message);
2185                            saveInDb = false;
2186                            message.setBody(decryptedBody);
2187                            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
2188                        }
2189                    }
2190                    break;
2191                case Message.ENCRYPTION_AXOLOTL:
2192                    message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
2193                    break;
2194            }
2195        }
2196
2197        synchronized (mScheduledMessages) {
2198            if (message.getTimeSent() > System.currentTimeMillis()) {
2199                mScheduledMessages.put(message.getUuid(), message);
2200                scheduleNextIdlePing();
2201            } else {
2202                mScheduledMessages.remove(message.getUuid());
2203            }
2204        }
2205
2206        boolean mucMessage =
2207                conversation.getMode() == Conversation.MODE_MULTI && !message.isPrivateMessage();
2208        if (mucMessage) {
2209            message.setCounterpart(conversation.getMucOptions().getSelf().getFullJid());
2210        }
2211
2212        if (resend) {
2213            if (packet != null && addToConversation) {
2214                if (account.getXmppConnection().getFeatures().sm() || mucMessage) {
2215                    markMessage(message, Message.STATUS_UNSEND);
2216                } else {
2217                    markMessage(message, Message.STATUS_SEND);
2218                }
2219            }
2220        } else {
2221            if (addToConversation) {
2222                conversation.add(message);
2223            }
2224            if (saveInDb) {
2225                databaseBackend.createMessage(message);
2226            } else if (message.edited()) {
2227                if (!databaseBackend.updateMessage(message, message.getEditedId())) {
2228                    Log.e(Config.LOGTAG, "error updated message in DB after edit");
2229                }
2230            }
2231            updateConversationUi();
2232        }
2233        if (packet != null) {
2234            if (delay) {
2235                mMessageGenerator.addDelay(packet, message.getTimeSent());
2236            }
2237            if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
2238                if (this.appSettings.isSendChatStates()) {
2239                    packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
2240                }
2241            }
2242            sendMessagePacket(account, packet);
2243        }
2244        if (!waitForPreview && !passedCbOn && cb != null) cb.run();
2245    }
2246
2247    public void sendUnsentMessages(final Conversation conversation) {
2248        conversation.findWaitingMessages(message -> resendMessage(message, true));
2249    }
2250
2251    public void resendMessage(final Message message, final boolean delay) {
2252        sendMessage(message, true, false, delay, false, null);
2253    }
2254
2255    public void resendMessage(final Message message, final boolean delay, final Runnable cb) {
2256        sendMessage(message, true, false, delay, false, cb);
2257    }
2258
2259    public void resendMessage(final Message message, final boolean delay, final boolean previewedLinks) {
2260        sendMessage(message, true, previewedLinks, delay, false, null);
2261    }
2262
2263    public Pair<Account,Account> onboardingIncomplete() {
2264        if (getAccounts().size() != 2) return null;
2265        Account onboarding = null;
2266        Account newAccount = null;
2267        for (final Account account : getAccounts()) {
2268            if (account.getJid().getDomain().equals(Config.ONBOARDING_DOMAIN)) {
2269                onboarding = account;
2270            } else {
2271                newAccount = account;
2272            }
2273        }
2274
2275        if (onboarding != null && newAccount != null) {
2276            return new Pair<>(onboarding, newAccount);
2277        }
2278
2279        return null;
2280    }
2281
2282    public boolean isOnboarding() {
2283        return getAccounts().size() == 1 && getAccounts().get(0).getJid().getDomain().equals(Config.ONBOARDING_DOMAIN);
2284    }
2285
2286    public void requestEasyOnboardingInvite(
2287            final Account account, final EasyOnboardingInvite.OnInviteRequested callback) {
2288        final var connection = account.getXmppConnection();
2289        final var discoManager = connection.getManager(DiscoManager.class);
2290        final var address = discoManager.getAddressForCommand(Namespace.EASY_ONBOARDING_INVITE);
2291        if (address == null) {
2292            callback.inviteRequestFailed(
2293                    getString(R.string.server_does_not_support_easy_onboarding_invites));
2294            return;
2295        }
2296        final Iq request = new Iq(Iq.Type.SET);
2297        request.setTo(address);
2298        final Element command = request.addChild("command", Namespace.COMMANDS);
2299        command.setAttribute("node", Namespace.EASY_ONBOARDING_INVITE);
2300        command.setAttribute("action", "execute");
2301        sendIqPacket(
2302                account,
2303                request,
2304                (response) -> {
2305                    if (response.getType() == Iq.Type.RESULT) {
2306                        final Element resultCommand =
2307                                response.findChild("command", Namespace.COMMANDS);
2308                        final Element x =
2309                                resultCommand == null
2310                                        ? null
2311                                        : resultCommand.findChild("x", Namespace.DATA);
2312                        if (x != null) {
2313                            final Data data = Data.parse(x);
2314                            final String uri = data.getValue("uri");
2315                            final String landingUrl = data.getValue("landing-url");
2316                            if (uri != null) {
2317                                final EasyOnboardingInvite invite =
2318                                        new EasyOnboardingInvite(
2319                                                address.getDomain().toString(), uri, landingUrl);
2320                                callback.inviteRequested(invite);
2321                                return;
2322                            }
2323                        }
2324                        callback.inviteRequestFailed(getString(R.string.unable_to_parse_invite));
2325                        Log.d(Config.LOGTAG, response.toString());
2326                    } else if (response.getType() == Iq.Type.ERROR) {
2327                        callback.inviteRequestFailed(IqParser.errorMessage(response));
2328                    } else {
2329                        callback.inviteRequestFailed(getString(R.string.remote_server_timeout));
2330                    }
2331                });
2332    }
2333
2334    public void markReadUpToStanzaId(final Conversation conversation, final String stanzaId) {
2335        final Message message = conversation.findMessageWithServerMsgId(stanzaId);
2336        if (message == null) { // do we want to check if isRead?
2337            return;
2338        }
2339        markReadUpTo(conversation, message);
2340    }
2341
2342    public void markReadUpTo(final Conversation conversation, final Message message) {
2343        final boolean isDismissNotification = isDismissNotification(message);
2344        final var uuid = message.getUuid();
2345        Log.d(
2346                Config.LOGTAG,
2347                conversation.getAccount().getJid().asBareJid()
2348                        + ": mark "
2349                        + conversation.getJid().asBareJid()
2350                        + " as read up to "
2351                        + uuid);
2352        markRead(conversation, uuid, isDismissNotification);
2353    }
2354
2355    private static boolean isDismissNotification(final Message message) {
2356        Message next = message.next();
2357        while (next != null) {
2358            if (message.getStatus() == Message.STATUS_RECEIVED) {
2359                return false;
2360            }
2361            next = next.next();
2362        }
2363        return true;
2364    }
2365
2366    public void createBookmark(final Account account, final Bookmark bookmark) {
2367        account.getXmppConnection().getManager(BookmarkManager.class).create(bookmark);
2368    }
2369
2370    public void deleteBookmark(final Account account, final Bookmark bookmark) {
2371        if (bookmark.getJid().toString().equals("discuss@conference.soprani.ca")) {
2372            getPreferences().edit().putBoolean("cheogram_sopranica_bookmark_deleted", true).apply();
2373        }
2374        account.getXmppConnection().getManager(BookmarkManager.class).delete(bookmark);
2375    }
2376
2377    private void restoreFromDatabase() {
2378        synchronized (this.conversations) {
2379            final Map<String, Account> accountLookupTable =
2380                    ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
2381            Log.d(Config.LOGTAG, "restoring conversations...");
2382            final long startTimeConversationsRestore = SystemClock.elapsedRealtime();
2383            this.conversations.addAll(
2384                    databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
2385            for (Iterator<Conversation> iterator = conversations.listIterator();
2386                    iterator.hasNext(); ) {
2387                Conversation conversation = iterator.next();
2388                Account account = accountLookupTable.get(conversation.getAccountUuid());
2389                if (account != null) {
2390                    conversation.setAccount(account);
2391                } else {
2392                    Log.e(Config.LOGTAG, "unable to restore Conversations with " + conversation.getJid());
2393                    conversations.remove(conversation);
2394                }
2395            }
2396            long diffConversationsRestore =
2397                    SystemClock.elapsedRealtime() - startTimeConversationsRestore;
2398            Log.d(
2399                    Config.LOGTAG,
2400                    "finished restoring conversations in " + diffConversationsRestore + "ms");
2401            Runnable runnable =
2402                    () -> {
2403                        if (DatabaseBackend.requiresMessageIndexRebuild()) {
2404                            DatabaseBackend.getInstance(this).rebuildMessagesIndex();
2405                        }
2406                        mutedMucUsers = databaseBackend.loadMutedMucUsers();
2407                        final long deletionDate = getAutomaticMessageDeletionDate();
2408                        mLastExpiryRun.set(SystemClock.elapsedRealtime());
2409                        if (deletionDate > 0) {
2410                            Log.d(
2411                                    Config.LOGTAG,
2412                                    "deleting messages that are older than "
2413                                            + AbstractGenerator.getTimestamp(deletionDate));
2414                            databaseBackend.expireOldMessages(deletionDate);
2415                        }
2416                        Log.d(Config.LOGTAG, "restoring roster...");
2417                        for (final Account account : accounts) {
2418                            account.getXmppConnection().getManager(RosterManager.class).restore();
2419                        }
2420                        getDrawableCache().evictAll();
2421                        loadPhoneContacts();
2422                        Log.d(Config.LOGTAG, "restoring messages...");
2423                        final long startMessageRestore = SystemClock.elapsedRealtime();
2424                        final Conversation quickLoad = QuickLoader.get(this.conversations);
2425                        if (quickLoad != null) {
2426                            restoreMessages(quickLoad);
2427                            updateConversationUi();
2428                            final long diffMessageRestore =
2429                                    SystemClock.elapsedRealtime() - startMessageRestore;
2430                            Log.d(
2431                                    Config.LOGTAG,
2432                                    "quickly restored "
2433                                            + quickLoad.getName()
2434                                            + " after "
2435                                            + diffMessageRestore
2436                                            + "ms");
2437                        }
2438                        for (Conversation conversation : this.conversations) {
2439                            if (quickLoad != conversation) {
2440                                restoreMessages(conversation);
2441                            }
2442                        }
2443                        mNotificationService.finishBacklog();
2444                        restoredFromDatabaseLatch.countDown();
2445                        final long diffMessageRestore =
2446                                SystemClock.elapsedRealtime() - startMessageRestore;
2447                        Log.d(
2448                                Config.LOGTAG,
2449                                "finished restoring messages in " + diffMessageRestore + "ms");
2450                        updateConversationUi();
2451                    };
2452            mDatabaseReaderExecutor.execute(
2453                    runnable); // will contain one write command (expiry) but that's fine
2454        }
2455    }
2456
2457    private void restoreMessages(Conversation conversation) {
2458        conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
2459        conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
2460        conversation.findMessagesAndCallsToNotify(mNotificationService::pushFromBacklog);
2461    }
2462
2463    public void loadPhoneContacts() {
2464        mContactMergerExecutor.execute(
2465                () -> {
2466                    final Map<Jid, JabberIdContact> contacts = JabberIdContact.load(this);
2467                    Log.d(Config.LOGTAG, "start merging phone contacts with roster");
2468                    // TODO if we do this merge this only on enabled accounts we need to trigger
2469                    // this upon enable
2470                    for (final Account account : accounts) {
2471                        final var remaining =
2472                                new ArrayList<>(
2473                                        account.getRoster()
2474                                                .getWithSystemAccounts(JabberIdContact.class));
2475                        for (final JabberIdContact jidContact : contacts.values()) {
2476                            final Contact contact =
2477                                    account.getRoster().getContact(jidContact.getJid());
2478                            boolean needsCacheClean = contact.setPhoneContact(jidContact);
2479                            if (needsCacheClean) {
2480                                getAvatarService().clear(contact);
2481                            }
2482                            remaining.remove(contact);
2483                        }
2484                        for (final Contact contact : remaining) {
2485                            boolean needsCacheClean =
2486                                    contact.unsetPhoneContact(JabberIdContact.class);
2487                            if (needsCacheClean) {
2488                                getAvatarService().clear(contact);
2489                            }
2490                        }
2491                    }
2492                    Log.d(Config.LOGTAG, "finished merging phone contacts");
2493                    mShortcutService.refresh(
2494                            mInitialAddressbookSyncCompleted.compareAndSet(false, true));
2495                    updateRosterUi(UpdateRosterReason.PUSH);
2496                    mQuickConversationsService.considerSync();
2497                });
2498    }
2499
2500    public List<Conversation> getConversations() {
2501        return this.conversations;
2502    }
2503
2504    private void markFileDeleted(final File file) {
2505        synchronized (FILENAMES_TO_IGNORE_DELETION) {
2506            if (FILENAMES_TO_IGNORE_DELETION.remove(file.getAbsolutePath())) {
2507                Log.d(Config.LOGTAG, "ignored deletion of " + file.getAbsolutePath());
2508                return;
2509            }
2510        }
2511        final boolean isInternalFile = fileBackend.isInternalFile(file);
2512        final List<String> uuids = databaseBackend.markFileAsDeleted(file, isInternalFile);
2513        Log.d(
2514                Config.LOGTAG,
2515                "deleted file "
2516                        + file.getAbsolutePath()
2517                        + " internal="
2518                        + isInternalFile
2519                        + ", database hits="
2520                        + uuids.size());
2521        markUuidsAsDeletedFiles(uuids);
2522    }
2523
2524    private void markUuidsAsDeletedFiles(List<String> uuids) {
2525        boolean deleted = false;
2526        for (Conversation conversation : getConversations()) {
2527            deleted |= conversation.markAsDeleted(uuids);
2528        }
2529        for (final String uuid : uuids) {
2530            evictPreview(uuid);
2531        }
2532        if (deleted) {
2533            updateConversationUi();
2534        }
2535    }
2536
2537    private void markChangedFiles(List<DatabaseBackend.FilePathInfo> infos) {
2538        boolean changed = false;
2539        for (Conversation conversation : getConversations()) {
2540            changed |= conversation.markAsChanged(infos);
2541        }
2542        if (changed) {
2543            updateConversationUi();
2544        }
2545    }
2546
2547    public void populateWithOrderedConversations(final List<Conversation> list) {
2548        populateWithOrderedConversations(list, true, true);
2549    }
2550
2551    public void populateWithOrderedConversations(
2552            final List<Conversation> list, final boolean includeNoFileUpload) {
2553        populateWithOrderedConversations(list, includeNoFileUpload, true);
2554    }
2555
2556    public void populateWithOrderedConversations(
2557            final List<Conversation> list, final boolean includeNoFileUpload, final boolean sort) {
2558        final List<String> orderedUuids;
2559        if (sort) {
2560            orderedUuids = null;
2561        } else {
2562            orderedUuids = new ArrayList<>();
2563            for (Conversation conversation : list) {
2564                orderedUuids.add(conversation.getUuid());
2565            }
2566        }
2567        list.clear();
2568        if (includeNoFileUpload) {
2569            list.addAll(getConversations());
2570        } else {
2571            for (Conversation conversation : getConversations()) {
2572                if (conversation.getMode() == Conversation.MODE_SINGLE
2573                        || (conversation.getAccount().httpUploadAvailable()
2574                                && conversation.getMucOptions().participating())) {
2575                    list.add(conversation);
2576                }
2577            }
2578        }
2579        try {
2580            if (orderedUuids != null) {
2581                Collections.sort(
2582                        list,
2583                        (a, b) -> {
2584                            final int indexA = orderedUuids.indexOf(a.getUuid());
2585                            final int indexB = orderedUuids.indexOf(b.getUuid());
2586                            if (indexA == -1 || indexB == -1 || indexA == indexB) {
2587                                return a.compareTo(b);
2588                            }
2589                            return indexA - indexB;
2590                        });
2591            } else {
2592                Collections.sort(list);
2593            }
2594        } catch (IllegalArgumentException e) {
2595            // ignore
2596        }
2597    }
2598
2599    public void loadMoreMessages(
2600            final Conversation conversation,
2601            final long timestamp,
2602            final OnMoreMessagesLoaded callback) {
2603        if (XmppConnectionService.this
2604                .getMessageArchiveService()
2605                .queryInProgress(conversation, callback)) {
2606            return;
2607        } else if (timestamp == 0) {
2608            return;
2609        }
2610        Log.d(
2611                Config.LOGTAG,
2612                "load more messages for "
2613                        + conversation.getName()
2614                        + " prior to "
2615                        + MessageGenerator.getTimestamp(timestamp));
2616        final Runnable runnable =
2617                () -> {
2618                    final Account account = conversation.getAccount();
2619                    List<Message> messages =
2620                            databaseBackend.getMessages(conversation, 50, timestamp);
2621                    if (messages.size() > 0) {
2622                        conversation.addAll(0, messages);
2623                        callback.onMoreMessagesLoaded(messages.size(), conversation);
2624                    } else if (conversation.hasMessagesLeftOnServer()
2625                            && account.isOnlineAndConnected()
2626                            && conversation.getLastClearHistory().getTimestamp() == 0) {
2627                        final boolean mamAvailable;
2628                        if (conversation.getMode() == Conversation.MODE_SINGLE) {
2629                            mamAvailable =
2630                                    account.getXmppConnection().getFeatures().mam()
2631                                            && !conversation.getContact().isBlocked();
2632                        } else {
2633                            mamAvailable = conversation.getMucOptions().mamSupport();
2634                        }
2635                        if (mamAvailable) {
2636                            MessageArchiveService.Query query =
2637                                    getMessageArchiveService()
2638                                            .query(
2639                                                    conversation,
2640                                                    new MamReference(0),
2641                                                    timestamp,
2642                                                    false);
2643                            if (query != null) {
2644                                query.setCallback(callback);
2645                                callback.informUser(R.string.fetching_history_from_server);
2646                            } else {
2647                                callback.informUser(R.string.not_fetching_history_retention_period);
2648                            }
2649                        }
2650                    }
2651                };
2652        mDatabaseReaderExecutor.execute(runnable);
2653    }
2654
2655    public List<Account> getAccounts() {
2656        return this.accounts;
2657    }
2658
2659    /**
2660     * This will find all conferences with the contact as member and also the conference that is the
2661     * contact (that 'fake' contact is used to store the avatar)
2662     */
2663    public List<Conversation> findAllConferencesWith(Contact contact) {
2664        final ArrayList<Conversation> results = new ArrayList<>();
2665        for (final Conversation c : conversations) {
2666            if (c.getMode() != Conversation.MODE_MULTI) {
2667                continue;
2668            }
2669            final MucOptions mucOptions = c.getMucOptions();
2670            if (c.getJid().asBareJid().equals(contact.getJid().asBareJid())
2671                    || (mucOptions != null && mucOptions.isContactInRoom(contact))) {
2672                results.add(c);
2673            }
2674        }
2675        return results;
2676    }
2677
2678    public Conversation find(final Contact contact) {
2679        for (final Conversation conversation : this.conversations) {
2680            if (conversation.getContact() == contact) {
2681                return conversation;
2682            }
2683        }
2684        return null;
2685    }
2686
2687    public Conversation find(
2688            final Iterable<Conversation> haystack, final Account account, final Jid jid) {
2689        if (jid == null) {
2690            return null;
2691        }
2692        for (final Conversation conversation : haystack) {
2693            if ((account == null || conversation.getAccount() == account)
2694                    && (conversation.getJid().asBareJid().equals(jid.asBareJid()))) {
2695                return conversation;
2696            }
2697        }
2698        return null;
2699    }
2700
2701    public boolean isConversationsListEmpty(final Conversation ignore) {
2702        synchronized (this.conversations) {
2703            final int size = this.conversations.size();
2704            return size == 0 || size == 1 && this.conversations.get(0) == ignore;
2705        }
2706    }
2707
2708    public boolean isConversationStillOpen(final Conversation conversation) {
2709        synchronized (this.conversations) {
2710            for (Conversation current : this.conversations) {
2711                if (current == conversation) {
2712                    return true;
2713                }
2714            }
2715        }
2716        return false;
2717    }
2718
2719    public void maybeRegisterWithMuc(Conversation c, String nickArg) {
2720        final var nick = nickArg == null ? c.getMucOptions().getSelf().getFullJid().getResource() : nickArg;
2721        final var register = new Iq(Iq.Type.GET);
2722        final var query0 = register.addChild("query");
2723        query0.setAttribute("xmlns", Namespace.REGISTER);
2724        register.setTo(c.getJid().asBareJid());
2725        sendIqPacket(c.getAccount(), register, (response) -> {
2726            if (response.getType() == Iq.Type.RESULT) {
2727                final Element query = response.addChild("query");
2728                query.setAttribute("xmlns", Namespace.REGISTER);
2729                String username = query.findChildContent("username", Namespace.REGISTER);
2730                if (username == null) username = query.findChildContent("nick", Namespace.REGISTER);
2731                if (username != null && username.equals(nick)) {
2732                    // Already registered with this nick, done
2733                    Log.d(Config.LOGTAG, "Already registered with " + c.getJid().asBareJid() + " as " + username);
2734                    return;
2735                }
2736                Data form = Data.parse(query.findChild("x", Namespace.DATA));
2737                if (form != null) {
2738                    final var field = form.getFieldByName("muc#register_roomnick");
2739                    if (field != null && nick.equals(field.getValue())) {
2740                        Log.d(Config.LOGTAG, "Already registered with " + c.getJid().asBareJid() + " as " + field.getValue());
2741                        return;
2742                    }
2743                }
2744                if (form == null || !"form".equals(form.getFormType()) || !form.getFields().stream().anyMatch(f -> f.isRequired() && !"muc#register_roomnick".equals(f.getFieldName()))) {
2745                    // No form, result form, or no required fields other than nickname, let's just send nickname
2746                    if (form == null || !"form".equals(form.getFormType())) {
2747                        form = new Data();
2748                        form.put("FORM_TYPE", "http://jabber.org/protocol/muc#register");
2749                    }
2750                    form.put("muc#register_roomnick", nick);
2751                    form.submit();
2752                    final var finish = new Iq(Iq.Type.SET);
2753                    final var query2 = finish.addChild("query");
2754                    query2.setAttribute("xmlns", Namespace.REGISTER);
2755                    query2.addChild(form);
2756                    finish.setTo(c.getJid().asBareJid());
2757                    sendIqPacket(c.getAccount(), finish, (response2) -> {
2758                        if (response.getType() == Iq.Type.RESULT) {
2759                            Log.w(Config.LOGTAG, "Success registering with channel " + c.getJid().asBareJid() + "/" + nick);
2760                        } else {
2761                            Log.w(Config.LOGTAG, "Error registering with channel: " + response2);
2762                        }
2763                    });
2764                } else {
2765                    // TODO: offer registration form to user
2766                    Log.d(Config.LOGTAG, "Complex registration form for " + c.getJid().asBareJid() + ": " + response);
2767                }
2768            } else {
2769                // We said maybe. Guess not
2770                Log.d(Config.LOGTAG, "Could not register with " + c.getJid().asBareJid() + ": " + response);
2771            }
2772        });
2773    }
2774
2775    public void deregisterWithMuc(Conversation c) {
2776        final Iq register = new Iq(Iq.Type.GET);
2777        final var query = register.addChild("query");
2778        query.setAttribute("xmlns", Namespace.REGISTER);
2779        query.addChild("remove");
2780        register.setTo(c.getJid().asBareJid());
2781        sendIqPacket(c.getAccount(), register, (response) -> {
2782            if (response.getType() == Iq.Type.RESULT) {
2783                Log.d(Config.LOGTAG, "deregistered with " + c.getJid().asBareJid());
2784            } else {
2785                Log.w(Config.LOGTAG, "Could not deregister with " + c.getJid().asBareJid() + ": " + response);
2786            }
2787        });
2788    }
2789
2790    public Conversation findOrCreateConversation(
2791            Account account, Jid jid, boolean muc, final boolean async) {
2792        return this.findOrCreateConversation(account, jid, muc, false, async);
2793    }
2794
2795    public Conversation findOrCreateConversation(
2796            final Account account,
2797            final Jid jid,
2798            final boolean muc,
2799            final boolean joinAfterCreate,
2800            final boolean async) {
2801        return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, null, async, null);
2802    }
2803
2804    public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final MessageArchiveService.Query query, final boolean async) {
2805        return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, query, async, null);
2806    }
2807
2808    public Conversation findOrCreateConversation(
2809            final Account account,
2810            final Jid jid,
2811            final boolean muc,
2812            final boolean joinAfterCreate,
2813            final MessageArchiveService.Query query,
2814            final boolean async,
2815            final String password) {
2816        synchronized (this.conversations) {
2817            final var cached = find(account, jid);
2818            if (cached != null) {
2819                return cached;
2820            }
2821            final var existing = databaseBackend.findConversation(account, jid);
2822            final Conversation conversation;
2823            final boolean loadMessagesFromDb;
2824            if (existing != null) {
2825                conversation = existing;
2826                if (password != null) conversation.getMucOptions().setPassword(password);
2827                loadMessagesFromDb = restoreFromArchive(conversation, jid, muc);
2828            } else {
2829                String conversationName;
2830                final Contact contact = account.getRoster().getContact(jid);
2831                if (contact != null) {
2832                    conversationName = contact.getDisplayName();
2833                } else {
2834                    conversationName = jid.getLocal();
2835                }
2836                if (muc) {
2837                    conversation =
2838                            new Conversation(
2839                                    conversationName, account, jid, Conversation.MODE_MULTI);
2840                } else {
2841                    conversation =
2842                            new Conversation(
2843                                    conversationName,
2844                                    account,
2845                                    jid.asBareJid(),
2846                                    Conversation.MODE_SINGLE);
2847                }
2848                if (password != null) conversation.getMucOptions().setPassword(password);
2849                this.databaseBackend.createConversation(conversation);
2850                loadMessagesFromDb = false;
2851            }
2852            if (async) {
2853                mDatabaseReaderExecutor.execute(
2854                        () ->
2855                                postProcessConversation(
2856                                        conversation, loadMessagesFromDb, joinAfterCreate, query));
2857            } else {
2858                postProcessConversation(conversation, loadMessagesFromDb, joinAfterCreate, query);
2859            }
2860            this.conversations.add(conversation);
2861            updateConversationUi();
2862            return conversation;
2863        }
2864    }
2865
2866    public Conversation findConversationByUuidReliable(final String uuid) {
2867        final var cached = findConversationByUuid(uuid);
2868        if (cached != null) {
2869            return cached;
2870        }
2871        final var existing = databaseBackend.findConversation(uuid);
2872        if (existing == null) {
2873            return null;
2874        }
2875        Log.d(Config.LOGTAG, "restoring conversation with " + existing.getJid() + " from DB");
2876        final Map<String, Account> accounts =
2877                ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
2878        final var account = accounts.get(existing.getAccountUuid());
2879        if (account == null) {
2880            Log.d(Config.LOGTAG, "could not find account " + existing.getAccountUuid());
2881            return null;
2882        }
2883        existing.setAccount(account);
2884        final var loadMessagesFromDb = restoreFromArchive(existing);
2885        mDatabaseReaderExecutor.execute(
2886                () ->
2887                        postProcessConversation(
2888                                existing,
2889                                loadMessagesFromDb,
2890                                existing.getMode() == Conversational.MODE_MULTI,
2891                                null));
2892        this.conversations.add(existing);
2893        if (existing.getMode() == Conversational.MODE_MULTI) {
2894            account.getXmppConnection()
2895                    .getManager(BookmarkManager.class)
2896                    .ensureBookmarkIsAutoJoin(existing);
2897        }
2898        updateConversationUi();
2899        return existing;
2900    }
2901
2902    private boolean restoreFromArchive(
2903            final Conversation conversation, final Jid jid, final boolean muc) {
2904        if (muc) {
2905            conversation.setMode(Conversation.MODE_MULTI);
2906            conversation.setContactJid(jid);
2907        } else {
2908            conversation.setMode(Conversation.MODE_SINGLE);
2909            conversation.setContactJid(jid.asBareJid());
2910        }
2911        return restoreFromArchive(conversation);
2912    }
2913
2914    private boolean restoreFromArchive(final Conversation conversation) {
2915        conversation.setStatus(Conversation.STATUS_AVAILABLE);
2916        databaseBackend.updateConversation(conversation);
2917        return conversation.messagesLoaded.compareAndSet(true, false);
2918    }
2919
2920    private void postProcessConversation(
2921            final Conversation c,
2922            final boolean loadMessagesFromDb,
2923            final boolean joinAfterCreate,
2924            final MessageArchiveService.Query query) {
2925        final var singleMode = c.getMode() == Conversational.MODE_SINGLE;
2926        final var account = c.getAccount();
2927        if (loadMessagesFromDb) {
2928            c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE));
2929            updateConversationUi();
2930            c.messagesLoaded.set(true);
2931        }
2932        if (account.getXmppConnection() != null
2933                && !c.getContact().isBlocked()
2934                && account.getXmppConnection().getFeatures().mam()
2935                && singleMode) {
2936            if (query == null) {
2937                mMessageArchiveService.query(c);
2938            } else {
2939                if (query.getConversation() == null) {
2940                    mMessageArchiveService.query(c, query.getStart(), query.isCatchup());
2941                }
2942            }
2943        }
2944        if (joinAfterCreate) {
2945            joinMuc(c);
2946        }
2947    }
2948
2949    public void archiveConversation(Conversation conversation) {
2950        archiveConversation(conversation, true);
2951    }
2952
2953    public void archiveConversation(
2954            Conversation conversation, final boolean maySynchronizeWithBookmarks) {
2955        if (isOnboarding()) return;
2956
2957        final var account = conversation.getAccount();
2958        final var connection = account.getXmppConnection();
2959        getNotificationService().clear(conversation);
2960        conversation.setStatus(Conversation.STATUS_ARCHIVED);
2961        conversation.setNextMessage(null);
2962        synchronized (this.conversations) {
2963            getMessageArchiveService().kill(conversation);
2964            if (conversation.getMode() == Conversation.MODE_MULTI) {
2965                // TODO always clean up bookmarks no matter if we are currently connected
2966                // TODO always delete reference to conversation in bookmark
2967                if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
2968                    final Bookmark bookmark = conversation.getBookmark();
2969                    if (maySynchronizeWithBookmarks && bookmark != null) {
2970                        if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) {
2971                            bookmark.setConversation(null);
2972                            deleteBookmark(account, bookmark);
2973                        } else if (bookmark.autojoin()) {
2974                            bookmark.setAutojoin(false);
2975                            createBookmark(bookmark.getAccount(), bookmark);
2976                        }
2977                    }
2978                }
2979                deregisterWithMuc(conversation);
2980                connection.getManager(MultiUserChatManager.class).leave(conversation);
2981            } else {
2982                if (conversation
2983                        .getContact()
2984                        .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
2985                    stopPresenceUpdatesTo(conversation.getContact());
2986                }
2987            }
2988            updateConversation(conversation);
2989            this.conversations.remove(conversation);
2990            updateConversationUi();
2991        }
2992    }
2993
2994    public void stopPresenceUpdatesTo(final Contact contact) {
2995        Log.d(Config.LOGTAG, "Canceling presence request from " + contact.getJid().toString());
2996        contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
2997        contact.getAccount()
2998                .getXmppConnection()
2999                .getManager(PresenceManager.class)
3000                .unsubscribed(contact.getJid().asBareJid());
3001    }
3002
3003    public void createAccount(final Account account) {
3004        account.setXmppConnection(createConnection(account));
3005        databaseBackend.createAccount(account);
3006        if (CallIntegration.hasSystemFeature(this)) {
3007            CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
3008        }
3009        this.accounts.add(account);
3010        this.reconnectAccountInBackground(account);
3011        updateAccountUi();
3012        syncEnabledAccountSetting();
3013        toggleForegroundService();
3014    }
3015
3016    private void syncEnabledAccountSetting() {
3017        final boolean hasEnabledAccounts = hasEnabledAccounts();
3018        getPreferences()
3019                .edit()
3020                .putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts)
3021                .apply();
3022        toggleSetProfilePictureActivity(hasEnabledAccounts);
3023    }
3024
3025    private void toggleSetProfilePictureActivity(final boolean enabled) {
3026        try {
3027            final ComponentName name =
3028                    new ComponentName(this, ChooseAccountForProfilePictureActivity.class);
3029            final int targetState =
3030                    enabled
3031                            ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
3032                            : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
3033            getPackageManager()
3034                    .setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP);
3035        } catch (IllegalStateException e) {
3036            Log.d(Config.LOGTAG, "unable to toggle profile picture activity");
3037        }
3038    }
3039
3040    public boolean reconfigurePushDistributor() {
3041        return this.unifiedPushBroker.reconfigurePushDistributor();
3042    }
3043
3044    private Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints(
3045            final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger) {
3046        return this.unifiedPushBroker.renewUnifiedPushEndpoints(pushTargetMessenger);
3047    }
3048
3049    public Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints() {
3050        return this.unifiedPushBroker.renewUnifiedPushEndpoints(null);
3051    }
3052
3053    public UnifiedPushBroker getUnifiedPushBroker() {
3054        return this.unifiedPushBroker;
3055    }
3056
3057    private void provisionAccount(final String address, final String password) {
3058        final Jid jid = Jid.of(address);
3059        final Account account = new Account(jid, password);
3060        account.setOption(Account.OPTION_DISABLED, true);
3061        Log.d(Config.LOGTAG, jid.asBareJid().toString() + ": provisioning account");
3062        createAccount(account);
3063    }
3064
3065    public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
3066        new Thread(
3067                        () -> {
3068                            try {
3069                                final X509Certificate[] chain =
3070                                        KeyChain.getCertificateChain(this, alias);
3071                                final X509Certificate cert =
3072                                        chain != null && chain.length > 0 ? chain[0] : null;
3073                                if (cert == null) {
3074                                    callback.informUser(R.string.unable_to_parse_certificate);
3075                                    return;
3076                                }
3077                                Pair<Jid, String> info = CryptoHelper.extractJidAndName(cert);
3078                                if (info == null) {
3079                                    callback.informUser(R.string.certificate_does_not_contain_jid);
3080                                    return;
3081                                }
3082                                if (findAccountByJid(info.first) == null) {
3083                                    final Account account = new Account(info.first, "");
3084                                    account.setPrivateKeyAlias(alias);
3085                                    account.setOption(Account.OPTION_DISABLED, true);
3086                                    account.setOption(Account.OPTION_FIXED_USERNAME, true);
3087                                    account.setDisplayName(info.second);
3088                                    createAccount(account);
3089                                    callback.onAccountCreated(account);
3090                                    if (Config.X509_VERIFICATION) {
3091                                        try {
3092                                            getMemorizingTrustManager()
3093                                                    .getNonInteractive(account.getServer(), null, 0, null)
3094                                                    .checkClientTrusted(chain, "RSA");
3095                                        } catch (CertificateException e) {
3096                                            callback.informUser(
3097                                                    R.string.certificate_chain_is_not_trusted);
3098                                        }
3099                                    }
3100                                } else {
3101                                    callback.informUser(R.string.account_already_exists);
3102                                }
3103                            } catch (Exception e) {
3104                                callback.informUser(R.string.unable_to_parse_certificate);
3105                            }
3106                        })
3107                .start();
3108    }
3109
3110    public void updateKeyInAccount(final Account account, final String alias) {
3111        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": update key in account " + alias);
3112        try {
3113            X509Certificate[] chain =
3114                    KeyChain.getCertificateChain(XmppConnectionService.this, alias);
3115            Log.d(Config.LOGTAG, account.getJid().asBareJid() + " loaded certificate chain");
3116            Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
3117            if (info == null) {
3118                showErrorToastInUi(R.string.certificate_does_not_contain_jid);
3119                return;
3120            }
3121            if (account.getJid().asBareJid().equals(info.first)) {
3122                account.setPrivateKeyAlias(alias);
3123                account.setDisplayName(info.second);
3124                databaseBackend.updateAccount(account);
3125                if (Config.X509_VERIFICATION) {
3126                    try {
3127                        getMemorizingTrustManager()
3128                                .getNonInteractive()
3129                                .checkClientTrusted(chain, "RSA");
3130                    } catch (CertificateException e) {
3131                        showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
3132                    }
3133                    account.getAxolotlService().regenerateKeys(true);
3134                }
3135            } else {
3136                showErrorToastInUi(R.string.jid_does_not_match_certificate);
3137            }
3138        } catch (Exception e) {
3139            e.printStackTrace();
3140        }
3141    }
3142
3143    public boolean updateAccount(final Account account) {
3144        if (databaseBackend.updateAccount(account)) {
3145            Integer color = account.getColorToSave();
3146            if (color == null) {
3147                getPreferences().edit().remove("account_color:" + account.getUuid()).commit();
3148            } else {
3149                getPreferences().edit().putInt("account_color:" + account.getUuid(), color.intValue()).commit();
3150            }
3151            account.setShowErrorNotification(true);
3152            // TODO what was the purpose of that? will likely be triggered by reconnect anyway?
3153            // this.statusListener.onStatusChanged(account);
3154            databaseBackend.updateAccount(account);
3155            reconnectAccountInBackground(account);
3156            updateAccountUi();
3157            getNotificationService().updateErrorNotification();
3158            toggleForegroundService();
3159            syncEnabledAccountSetting();
3160            mChannelDiscoveryService.cleanCache();
3161            if (CallIntegration.hasSystemFeature(this)) {
3162                CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
3163            }
3164            return true;
3165        } else {
3166            return false;
3167        }
3168    }
3169
3170    public ListenableFuture<Void> updateAccountPasswordOnServer(
3171            final Account account, final String newPassword) {
3172        final var connection = account.getXmppConnection();
3173        return connection.getManager(RegistrationManager.class).setPassword(newPassword);
3174    }
3175
3176    public void deleteAccount(final Account account) {
3177        getPreferences().edit().remove("onboarding_continued").commit();
3178        final boolean connected = account.getStatus() == Account.State.ONLINE;
3179        synchronized (this.conversations) {
3180            if (connected) {
3181                account.getAxolotlService().deleteOmemoIdentity();
3182            }
3183            for (final Conversation conversation : conversations) {
3184                if (conversation.getAccount() == account) {
3185                    if (conversation.getMode() == Conversation.MODE_MULTI) {
3186                        if (connected) {
3187                            account.getXmppConnection()
3188                                    .getManager(MultiUserChatManager.class)
3189                                    .unavailable(conversation);
3190                        }
3191                    }
3192                    conversations.remove(conversation);
3193                    mNotificationService.clear(conversation);
3194                }
3195            }
3196            new Thread(() -> {
3197                for (final Contact contact : account.getRoster().getContacts()) {
3198                    contact.unregisterAsPhoneAccount(this);
3199                }
3200            }).start();
3201            if (account.getXmppConnection() != null) {
3202                new Thread(() -> disconnect(account, !connected)).start();
3203            }
3204            final Runnable runnable =
3205                    () -> {
3206                        if (!databaseBackend.deleteAccount(account)) {
3207                            Log.d(
3208                                    Config.LOGTAG,
3209                                    account.getJid().asBareJid() + ": unable to delete account");
3210                        }
3211                    };
3212            mDatabaseWriterExecutor.execute(runnable);
3213            this.accounts.remove(account);
3214            if (CallIntegration.hasSystemFeature(this)) {
3215                CallIntegrationConnectionService.unregisterPhoneAccount(this, account);
3216            }
3217            updateAccountUi();
3218            mNotificationService.updateErrorNotification();
3219            syncEnabledAccountSetting();
3220            toggleForegroundService();
3221        }
3222    }
3223
3224    public void setOnConversationListChangedListener(OnConversationUpdate listener) {
3225        final boolean remainingListeners;
3226        synchronized (LISTENER_LOCK) {
3227            remainingListeners = checkListeners();
3228            if (!this.mOnConversationUpdates.add(listener)) {
3229                Log.w(
3230                        Config.LOGTAG,
3231                        listener.getClass().getName()
3232                                + " is already registered as ConversationListChangedListener");
3233            }
3234            this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
3235        }
3236        if (remainingListeners) {
3237            switchToForeground();
3238        }
3239    }
3240
3241    public void removeOnConversationListChangedListener(OnConversationUpdate listener) {
3242        final boolean remainingListeners;
3243        synchronized (LISTENER_LOCK) {
3244            this.mOnConversationUpdates.remove(listener);
3245            this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
3246            remainingListeners = checkListeners();
3247        }
3248        if (remainingListeners) {
3249            switchToBackground();
3250        }
3251    }
3252
3253    public void setOnShowErrorToastListener(OnShowErrorToast listener) {
3254        final boolean remainingListeners;
3255        synchronized (LISTENER_LOCK) {
3256            remainingListeners = checkListeners();
3257            if (!this.mOnShowErrorToasts.add(listener)) {
3258                Log.w(
3259                        Config.LOGTAG,
3260                        listener.getClass().getName()
3261                                + " is already registered as OnShowErrorToastListener");
3262            }
3263        }
3264        if (remainingListeners) {
3265            switchToForeground();
3266        }
3267    }
3268
3269    public void removeOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
3270        final boolean remainingListeners;
3271        synchronized (LISTENER_LOCK) {
3272            this.mOnShowErrorToasts.remove(onShowErrorToast);
3273            remainingListeners = checkListeners();
3274        }
3275        if (remainingListeners) {
3276            switchToBackground();
3277        }
3278    }
3279
3280    public void setOnAccountListChangedListener(OnAccountUpdate listener) {
3281        final boolean remainingListeners;
3282        synchronized (LISTENER_LOCK) {
3283            remainingListeners = checkListeners();
3284            if (!this.mOnAccountUpdates.add(listener)) {
3285                Log.w(
3286                        Config.LOGTAG,
3287                        listener.getClass().getName()
3288                                + " is already registered as OnAccountListChangedtListener");
3289            }
3290        }
3291        if (remainingListeners) {
3292            switchToForeground();
3293        }
3294    }
3295
3296    public void removeOnAccountListChangedListener(OnAccountUpdate listener) {
3297        final boolean remainingListeners;
3298        synchronized (LISTENER_LOCK) {
3299            this.mOnAccountUpdates.remove(listener);
3300            remainingListeners = checkListeners();
3301        }
3302        if (remainingListeners) {
3303            switchToBackground();
3304        }
3305    }
3306
3307    public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
3308        final boolean remainingListeners;
3309        synchronized (LISTENER_LOCK) {
3310            remainingListeners = checkListeners();
3311            if (!this.mOnCaptchaRequested.add(listener)) {
3312                Log.w(
3313                        Config.LOGTAG,
3314                        listener.getClass().getName()
3315                                + " is already registered as OnCaptchaRequestListener");
3316            }
3317        }
3318        if (remainingListeners) {
3319            switchToForeground();
3320        }
3321    }
3322
3323    public void removeOnCaptchaRequestedListener(OnCaptchaRequested listener) {
3324        final boolean remainingListeners;
3325        synchronized (LISTENER_LOCK) {
3326            this.mOnCaptchaRequested.remove(listener);
3327            remainingListeners = checkListeners();
3328        }
3329        if (remainingListeners) {
3330            switchToBackground();
3331        }
3332    }
3333
3334    public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
3335        final boolean remainingListeners;
3336        synchronized (LISTENER_LOCK) {
3337            remainingListeners = checkListeners();
3338            if (!this.mOnRosterUpdates.add(listener)) {
3339                Log.w(
3340                        Config.LOGTAG,
3341                        listener.getClass().getName()
3342                                + " is already registered as OnRosterUpdateListener");
3343            }
3344        }
3345        if (remainingListeners) {
3346            switchToForeground();
3347        }
3348    }
3349
3350    public void removeOnRosterUpdateListener(final OnRosterUpdate listener) {
3351        final boolean remainingListeners;
3352        synchronized (LISTENER_LOCK) {
3353            this.mOnRosterUpdates.remove(listener);
3354            remainingListeners = checkListeners();
3355        }
3356        if (remainingListeners) {
3357            switchToBackground();
3358        }
3359    }
3360
3361    public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
3362        final boolean remainingListeners;
3363        synchronized (LISTENER_LOCK) {
3364            remainingListeners = checkListeners();
3365            if (!this.mOnUpdateBlocklist.add(listener)) {
3366                Log.w(
3367                        Config.LOGTAG,
3368                        listener.getClass().getName()
3369                                + " is already registered as OnUpdateBlocklistListener");
3370            }
3371        }
3372        if (remainingListeners) {
3373            switchToForeground();
3374        }
3375    }
3376
3377    public void removeOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
3378        final boolean remainingListeners;
3379        synchronized (LISTENER_LOCK) {
3380            this.mOnUpdateBlocklist.remove(listener);
3381            remainingListeners = checkListeners();
3382        }
3383        if (remainingListeners) {
3384            switchToBackground();
3385        }
3386    }
3387
3388    public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
3389        final boolean remainingListeners;
3390        synchronized (LISTENER_LOCK) {
3391            remainingListeners = checkListeners();
3392            if (!this.mOnKeyStatusUpdated.add(listener)) {
3393                Log.w(
3394                        Config.LOGTAG,
3395                        listener.getClass().getName()
3396                                + " is already registered as OnKeyStatusUpdateListener");
3397            }
3398        }
3399        if (remainingListeners) {
3400            switchToForeground();
3401        }
3402    }
3403
3404    public void removeOnNewKeysAvailableListener(final OnKeyStatusUpdated listener) {
3405        final boolean remainingListeners;
3406        synchronized (LISTENER_LOCK) {
3407            this.mOnKeyStatusUpdated.remove(listener);
3408            remainingListeners = checkListeners();
3409        }
3410        if (remainingListeners) {
3411            switchToBackground();
3412        }
3413    }
3414
3415    public void setOnRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
3416        final boolean remainingListeners;
3417        synchronized (LISTENER_LOCK) {
3418            remainingListeners = checkListeners();
3419            if (!this.onJingleRtpConnectionUpdate.add(listener)) {
3420                Log.w(
3421                        Config.LOGTAG,
3422                        listener.getClass().getName()
3423                                + " is already registered as OnJingleRtpConnectionUpdate");
3424            }
3425        }
3426        if (remainingListeners) {
3427            switchToForeground();
3428        }
3429    }
3430
3431    public void removeRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
3432        final boolean remainingListeners;
3433        synchronized (LISTENER_LOCK) {
3434            this.onJingleRtpConnectionUpdate.remove(listener);
3435            remainingListeners = checkListeners();
3436        }
3437        if (remainingListeners) {
3438            switchToBackground();
3439        }
3440    }
3441
3442    public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
3443        final boolean remainingListeners;
3444        synchronized (LISTENER_LOCK) {
3445            remainingListeners = checkListeners();
3446            if (!this.mOnMucRosterUpdate.add(listener)) {
3447                Log.w(
3448                        Config.LOGTAG,
3449                        listener.getClass().getName()
3450                                + " is already registered as OnMucRosterListener");
3451            }
3452        }
3453        if (remainingListeners) {
3454            switchToForeground();
3455        }
3456    }
3457
3458    public void removeOnMucRosterUpdateListener(final OnMucRosterUpdate listener) {
3459        final boolean remainingListeners;
3460        synchronized (LISTENER_LOCK) {
3461            this.mOnMucRosterUpdate.remove(listener);
3462            remainingListeners = checkListeners();
3463        }
3464        if (remainingListeners) {
3465            switchToBackground();
3466        }
3467    }
3468
3469    public boolean checkListeners() {
3470        return (this.mOnAccountUpdates.isEmpty()
3471                && this.mOnConversationUpdates.isEmpty()
3472                && this.mOnRosterUpdates.isEmpty()
3473                && this.mOnCaptchaRequested.isEmpty()
3474                && this.mOnMucRosterUpdate.isEmpty()
3475                && this.mOnUpdateBlocklist.isEmpty()
3476                && this.mOnShowErrorToasts.isEmpty()
3477                && this.onJingleRtpConnectionUpdate.isEmpty()
3478                && this.mOnKeyStatusUpdated.isEmpty());
3479    }
3480
3481    private void switchToForeground() {
3482        toggleSoftDisabled(false);
3483        final boolean broadcastLastActivity = appSettings.isBroadcastLastActivity();
3484        for (Conversation conversation : getConversations()) {
3485            if (conversation.getMode() == Conversation.MODE_MULTI) {
3486                conversation.getMucOptions().resetChatState();
3487            } else {
3488                conversation.setIncomingChatState(Config.DEFAULT_CHAT_STATE);
3489            }
3490        }
3491        for (final var account : getAccounts()) {
3492            if (account.getStatus() != Account.State.ONLINE) {
3493                continue;
3494            }
3495            account.deactivateGracePeriod();
3496            final XmppConnection connection = account.getXmppConnection();
3497            if (connection.getFeatures().csi()) {
3498                connection.sendActive();
3499            }
3500            if (broadcastLastActivity) {
3501                // send new presence but don't include idle because we are not
3502                connection.getManager(PresenceManager.class).available(false);
3503            }
3504        }
3505        Log.d(Config.LOGTAG, "app switched into foreground");
3506    }
3507
3508    private void switchToBackground() {
3509        final boolean broadcastLastActivity = appSettings.isBroadcastLastActivity();
3510        if (broadcastLastActivity) {
3511            mLastActivity = System.currentTimeMillis();
3512            final SharedPreferences.Editor editor = getPreferences().edit();
3513            editor.putLong(SETTING_LAST_ACTIVITY_TS, mLastActivity);
3514            editor.apply();
3515        }
3516        for (final var account : getAccounts()) {
3517            if (account.getStatus() != Account.State.ONLINE) {
3518                continue;
3519            }
3520            final var connection = account.getXmppConnection();
3521            if (broadcastLastActivity) {
3522                connection.getManager(PresenceManager.class).available(true);
3523            }
3524            if (connection.getFeatures().csi()) {
3525                connection.sendInactive();
3526            }
3527        }
3528        this.mNotificationService.setIsInForeground(false);
3529        Log.d(Config.LOGTAG, "app switched into background");
3530    }
3531
3532    public void connectMultiModeConversations(Account account) {
3533        List<Conversation> conversations = getConversations();
3534        for (Conversation conversation : conversations) {
3535            if (conversation.getMode() == Conversation.MODE_MULTI
3536                    && conversation.getAccount() == account) {
3537                joinMuc(conversation);
3538            }
3539        }
3540    }
3541
3542    public void joinMuc(final Conversation conversation) {
3543        final var account = conversation.getAccount();
3544        account.getXmppConnection().getManager(MultiUserChatManager.class).join(conversation);
3545    }
3546
3547    public void providePasswordForMuc(final Conversation conversation, final String password) {
3548        final var account = conversation.getAccount();
3549        account.getXmppConnection()
3550                .getManager(MultiUserChatManager.class)
3551                .setPassword(conversation, password);
3552    }
3553
3554    public void deleteAvatar(final Account account) {
3555        final var connection = account.getXmppConnection();
3556
3557        final var vCardPhotoDeletionFuture =
3558                connection.getManager(VCardManager.class).deletePhoto();
3559        final var pepDeletionFuture = connection.getManager(AvatarManager.class).delete();
3560
3561        final var deletionFuture = Futures.allAsList(vCardPhotoDeletionFuture, pepDeletionFuture);
3562
3563        Futures.addCallback(
3564                deletionFuture,
3565                new FutureCallback<>() {
3566                    @Override
3567                    public void onSuccess(List<Void> result) {
3568                        Log.d(
3569                                Config.LOGTAG,
3570                                account.getJid().asBareJid() + ": deleted avatar from server");
3571                        account.setAvatar(null);
3572                        databaseBackend.updateAccount(account);
3573                        getAvatarService().clear(account);
3574                        updateAccountUi();
3575                    }
3576
3577                    @Override
3578                    public void onFailure(Throwable t) {
3579                        Log.d(
3580                                Config.LOGTAG,
3581                                account.getJid().asBareJid() + ": could not delete avatar",
3582                                t);
3583                    }
3584                },
3585                MoreExecutors.directExecutor());
3586    }
3587
3588    public void deletePepNode(final Account account, final String node) {
3589        final var future = account.getXmppConnection().getManager(PepManager.class).delete(node);
3590        Futures.addCallback(
3591                future,
3592                new FutureCallback<Void>() {
3593                    @Override
3594                    public void onSuccess(Void result) {
3595                        Log.d(
3596                                Config.LOGTAG,
3597                                account.getJid().asBareJid()
3598                                        + ": successfully deleted pep node "
3599                                        + node);
3600                    }
3601
3602                    @Override
3603                    public void onFailure(@NonNull Throwable t) {
3604                        Log.d(
3605                                Config.LOGTAG,
3606                                account.getJid().asBareJid() + ": failed to delete node " + node,
3607                                t);
3608                    }
3609                },
3610                MoreExecutors.directExecutor());
3611    }
3612
3613    private boolean hasEnabledAccounts() {
3614        if (this.accounts == null) {
3615            return false;
3616        }
3617        for (final Account account : this.accounts) {
3618            if (account.isConnectionEnabled()) {
3619                return true;
3620            }
3621        }
3622        return false;
3623    }
3624
3625    public void getAttachments(
3626            final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) {
3627        getAttachments(
3628                conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded);
3629    }
3630
3631    public void getAttachments(
3632            final Account account,
3633            final Jid jid,
3634            final int limit,
3635            final OnMediaLoaded onMediaLoaded) {
3636        getAttachments(account.getUuid(), jid.asBareJid(), limit, onMediaLoaded);
3637    }
3638
3639    public void getAttachments(
3640            final String account,
3641            final Jid jid,
3642            final int limit,
3643            final OnMediaLoaded onMediaLoaded) {
3644        new Thread(
3645                        () ->
3646                                onMediaLoaded.onMediaLoaded(
3647                                        fileBackend.convertToAttachments(
3648                                                databaseBackend.getRelativeFilePaths(
3649                                                        account, jid, limit))))
3650                .start();
3651    }
3652
3653    public void persistSelfNick(final MucOptions.User self, final boolean modified) {
3654        final Conversation conversation = self.getConversation();
3655        final Account account = conversation.getAccount();
3656        final Jid full = self.getFullJid();
3657        if (!full.equals(conversation.getJid())) {
3658            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persisting full jid " + full);
3659            conversation.setContactJid(full);
3660            databaseBackend.updateConversation(conversation);
3661        }
3662
3663        final String nick = self.getNick();
3664        final Bookmark bookmark = conversation.getBookmark();
3665        if (bookmark == null || !modified) {
3666            return;
3667        }
3668        final String defaultNick = MucOptions.defaultNick(account);
3669        if (nick.equals(defaultNick) || nick.equals(bookmark.getNick())) {
3670            return;
3671        }
3672        Log.d(
3673                Config.LOGTAG,
3674                account.getJid().asBareJid()
3675                        + ": persist nick '"
3676                        + full.getResource()
3677                        + "' into bookmark for "
3678                        + conversation.getJid().asBareJid());
3679        bookmark.setNick(nick);
3680        createBookmark(bookmark.getAccount(), bookmark);
3681    }
3682
3683    public void checkMucRequiresRename() {
3684        synchronized (this.conversations) {
3685            for (final Conversation conversation : this.conversations) {
3686                if (conversation.getMode() == Conversational.MODE_MULTI) {
3687                    final var account = conversation.getAccount();
3688                    account.getXmppConnection()
3689                            .getManager(MultiUserChatManager.class)
3690                            .checkMucRequiresRename(conversation);
3691                }
3692            }
3693        }
3694    }
3695
3696    public void createPublicChannel(
3697            final Account account,
3698            final String name,
3699            final Jid address,
3700            final UiCallback<Conversation> callback) {
3701        final var future =
3702                account.getXmppConnection()
3703                        .getManager(MultiUserChatManager.class)
3704                        .createPublicChannel(address, name);
3705
3706        Futures.addCallback(
3707                future,
3708                new FutureCallback<Conversation>() {
3709                    @Override
3710                    public void onSuccess(Conversation result) {
3711                        callback.success(result);
3712                    }
3713
3714                    @Override
3715                    public void onFailure(Throwable t) {
3716                        Log.d(Config.LOGTAG, "could not create public channel", t);
3717                        // TODO I guess it’s better to just not use callbacks here
3718                        callback.error(R.string.unable_to_set_channel_configuration, null);
3719                    }
3720                },
3721                MoreExecutors.directExecutor());
3722    }
3723
3724    public void checkIfMuc(final Account account, final Jid jid, Consumer<Boolean> cb) {
3725        if (jid.isDomainJid()) {
3726            // Spec basically says MUC needs to have a node
3727            // And also specifies that MUC and MUC service should have the same identity...
3728            cb.accept(false);
3729            return;
3730        }
3731
3732        final var connection = account.getXmppConnection();
3733        if (connection == null) {
3734            cb.accept(false); // hmmm...
3735            return;
3736        }
3737        final ListenableFuture<InfoQuery> future =
3738                connection
3739                        .getManager(DiscoManager.class)
3740                        .info(Entity.discoItem(jid), null);
3741
3742        Futures.addCallback(
3743                future,
3744                new FutureCallback<>() {
3745                    @Override
3746                    public void onSuccess(InfoQuery result) {
3747                        cb.accept(
3748                            result.hasFeature("http://jabber.org/protocol/muc") &&
3749                            result.hasIdentityWithCategory("conference")
3750                        );
3751                    }
3752
3753                    @Override
3754                    public void onFailure(@NonNull Throwable throwable) {
3755                        cb.accept(false);
3756                    }
3757                },
3758                MoreExecutors.directExecutor()
3759        );
3760    }
3761
3762    public boolean createAdhocConference(
3763            final Account account,
3764            final String name,
3765            final Collection<Jid> addresses,
3766            final UiCallback<Conversation> callback) {
3767        final var manager = account.getXmppConnection().getManager(MultiUserChatManager.class);
3768        if (manager.getServices().isEmpty()) {
3769            return false;
3770        }
3771
3772        final var future = manager.createPrivateGroupChat(name, addresses);
3773
3774        Futures.addCallback(
3775                future,
3776                new FutureCallback<>() {
3777                    @Override
3778                    public void onSuccess(Conversation result) {
3779                        callback.success(result);
3780                    }
3781
3782                    @Override
3783                    public void onFailure(@NonNull Throwable t) {
3784                        Log.d(Config.LOGTAG, "could not create private group chat", t);
3785                        callback.error(R.string.conference_creation_failed, null);
3786                    }
3787                },
3788                MoreExecutors.directExecutor());
3789        return true;
3790    }
3791
3792    public void pushNodeConfiguration(
3793            Account account,
3794            final String node,
3795            final Bundle options,
3796            final OnConfigurationPushed callback) {
3797        pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback);
3798    }
3799
3800    public void pushNodeConfiguration(
3801            Account account,
3802            final Jid jid,
3803            final String node,
3804            final Bundle options,
3805            final OnConfigurationPushed callback) {
3806        Log.d(Config.LOGTAG, "pushing node configuration");
3807        sendIqPacket(
3808                account,
3809                mIqGenerator.requestPubsubConfiguration(jid, node),
3810                responseToRequest -> {
3811                    if (responseToRequest.getType() == Iq.Type.RESULT) {
3812                        Element pubsub =
3813                                responseToRequest.findChild(
3814                                        "pubsub", "http://jabber.org/protocol/pubsub#owner");
3815                        Element configuration =
3816                                pubsub == null ? null : pubsub.findChild("configure");
3817                        Element x =
3818                                configuration == null
3819                                        ? null
3820                                        : configuration.findChild("x", Namespace.DATA);
3821                        if (x != null) {
3822                            final Data data = Data.parse(x);
3823                            data.submit(options);
3824                            sendIqPacket(
3825                                    account,
3826                                    mIqGenerator.publishPubsubConfiguration(jid, node, data),
3827                                    responseToPublish -> {
3828                                        if (responseToPublish.getType() == Iq.Type.RESULT
3829                                                && callback != null) {
3830                                            Log.d(
3831                                                    Config.LOGTAG,
3832                                                    account.getJid().asBareJid()
3833                                                            + ": successfully changed node"
3834                                                            + " configuration for node "
3835                                                            + node);
3836                                            callback.onPushSucceeded();
3837                                        } else if (responseToPublish.getType() == Iq.Type.ERROR
3838                                                && callback != null) {
3839                                            callback.onPushFailed();
3840                                        }
3841                                    });
3842                        } else if (callback != null) {
3843                            callback.onPushFailed();
3844                        }
3845                    } else if (responseToRequest.getType() == Iq.Type.ERROR && callback != null) {
3846                        callback.onPushFailed();
3847                    }
3848                });
3849    }
3850
3851    public void pushSubjectToConference(final Conversation conference, final String subject) {
3852        final var account = conference.getAccount();
3853        account.getXmppConnection()
3854                .getManager(MultiUserChatManager.class)
3855                .setSubject(conference, subject);
3856    }
3857
3858    public void requestVoice(final Account account, final Jid jid) {
3859        final var packet = this.getMessageGenerator().requestVoice(jid);
3860        this.sendMessagePacket(account, packet);
3861    }
3862
3863    public void changeAffiliationInConference(
3864            final Conversation conference,
3865            Jid user,
3866            final Affiliation affiliation,
3867            final OnAffiliationChanged callback) {
3868        final var account = conference.getAccount();
3869        final var future =
3870                account.getXmppConnection()
3871                        .getManager(MultiUserChatManager.class)
3872                        .setAffiliation(conference, affiliation, user);
3873        Futures.addCallback(
3874                future,
3875                new FutureCallback<Void>() {
3876                    @Override
3877                    public void onSuccess(Void result) {
3878                        if (callback != null) {
3879                            callback.onAffiliationChangedSuccessful(user);
3880                        } else {
3881                            Log.d(
3882                                    Config.LOGTAG,
3883                                    "changed affiliation of " + user + " to " + affiliation);
3884                        }
3885                    }
3886
3887                    @Override
3888                    public void onFailure(Throwable t) {
3889                        if (callback != null) {
3890                            callback.onAffiliationChangeFailed(
3891                                    user, R.string.could_not_change_affiliation);
3892                        } else {
3893                            Log.d(Config.LOGTAG, "could not change affiliation", t);
3894                        }
3895                    }
3896                },
3897                MoreExecutors.directExecutor());
3898    }
3899
3900    public void changeRoleInConference(
3901            final Conversation conference, final String nick, Role role) {
3902        final var account = conference.getAccount();
3903        account.getXmppConnection()
3904                .getManager(MultiUserChatManager.class)
3905                .setRole(conference.getJid().asBareJid(), role, nick);
3906    }
3907
3908    public void moderateMessage(final Account account, final Message m, final String reason) {
3909        final var request = this.mIqGenerator.moderateMessage(account, m, reason);
3910        sendIqPacket(account, request, (packet) -> {
3911            if (packet.getType() != Iq.Type.RESULT) {
3912                showErrorToastInUi(R.string.unable_to_moderate);
3913                Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to moderate: " + packet);
3914            }
3915        });
3916    }
3917
3918    public ListenableFuture<Void> destroyRoom(final Conversation conversation) {
3919        final var account = conversation.getAccount();
3920        return account.getXmppConnection()
3921                .getManager(MultiUserChatManager.class)
3922                .destroy(conversation.getJid().asBareJid());
3923    }
3924
3925    private void disconnect(final Account account, boolean force) {
3926        final XmppConnection connection = account.getXmppConnection();
3927        if (connection == null) {
3928            return;
3929        }
3930        if (!force) {
3931            final List<Conversation> conversations = getConversations();
3932            for (Conversation conversation : conversations) {
3933                if (conversation.getAccount() == account) {
3934                    if (conversation.getMode() == Conversation.MODE_MULTI) {
3935                        account.getXmppConnection()
3936                                .getManager(MultiUserChatManager.class)
3937                                .unavailable(conversation);
3938                    }
3939                }
3940            }
3941            connection.getManager(PresenceManager.class).unavailable();
3942        }
3943        connection.disconnect(force);
3944    }
3945
3946    @Override
3947    public IBinder onBind(Intent intent) {
3948        return mBinder;
3949    }
3950
3951    public void deleteMessage(Message message) {
3952        mScheduledMessages.remove(message.getUuid());
3953        databaseBackend.deleteMessage(message.getUuid());
3954        ((Conversation) message.getConversation()).remove(message);
3955        updateConversationUi();
3956    }
3957
3958    public void updateMessage(Message message) {
3959        updateMessage(message, true);
3960    }
3961
3962    public void updateMessage(Message message, boolean includeBody) {
3963        databaseBackend.updateMessage(message, includeBody);
3964        updateConversationUi();
3965    }
3966
3967    public void createMessageAsync(final Message message) {
3968        mDatabaseWriterExecutor.execute(() -> databaseBackend.createMessage(message));
3969    }
3970
3971    public void updateMessage(Message message, String uuid) {
3972        if (!databaseBackend.updateMessage(message, uuid)) {
3973            Log.e(Config.LOGTAG, "error updated message in DB after edit");
3974        }
3975        updateConversationUi();
3976    }
3977
3978    public void createContact(final Contact contact) {
3979        createContact(contact, null);
3980    }
3981
3982    public void unregisterPhoneAccounts(final Account account) {
3983        for (final Contact contact : account.getRoster().getContacts()) {
3984            if (!contact.showInRoster()) {
3985                contact.unregisterAsPhoneAccount(this);
3986            }
3987        }
3988    }
3989
3990    public void createContact(final Contact contact, final String preAuth) {
3991        contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
3992        contact.setOption(Contact.Options.ASKING);
3993        final var connection = contact.getAccount().getXmppConnection();
3994        connection.getManager(RosterManager.class).addRosterItem(contact, preAuth);
3995    }
3996
3997    public void deleteContactOnServer(final Contact contact) {
3998        final var connection = contact.getAccount().getXmppConnection();
3999        connection.getManager(RosterManager.class).deleteRosterItem(contact);
4000    }
4001
4002    public void publishMucAvatar(
4003            final Conversation conversation, final Uri image, final OnAvatarPublication callback) {
4004        final var connection = conversation.getAccount().getXmppConnection();
4005        final var future =
4006                connection
4007                        .getManager(AvatarManager.class)
4008                        .publishVCard(conversation.getJid().asBareJid(), image);
4009        Futures.addCallback(
4010                future,
4011                new FutureCallback<>() {
4012                    @Override
4013                    public void onSuccess(Void result) {
4014                        callback.onAvatarPublicationSucceeded();
4015                    }
4016
4017                    @Override
4018                    public void onFailure(@NonNull Throwable t) {
4019                        Log.d(Config.LOGTAG, "could not publish MUC avatar", t);
4020                        callback.onAvatarPublicationFailed(
4021                                R.string.error_publish_avatar_server_reject);
4022                    }
4023                },
4024                MoreExecutors.directExecutor());
4025    }
4026
4027    public void publishAvatar(
4028            final Account account,
4029            final Uri image,
4030            final boolean open,
4031            final OnAvatarPublication callback) {
4032
4033        final var connection = account.getXmppConnection();
4034        final var publicationFuture =
4035                connection.getManager(AvatarManager.class).uploadAndPublish(image, open);
4036
4037        Futures.addCallback(
4038                publicationFuture,
4039                new FutureCallback<>() {
4040                    @Override
4041                    public void onSuccess(final Void result) {
4042                        Log.d(Config.LOGTAG, "published avatar");
4043                        callback.onAvatarPublicationSucceeded();
4044                    }
4045
4046                    @Override
4047                    public void onFailure(@NonNull final Throwable t) {
4048                        Log.d(Config.LOGTAG, "avatar upload failed", t);
4049                        // TODO actually figure out what went wrong
4050                        callback.onAvatarPublicationFailed(
4051                                R.string.error_publish_avatar_server_reject);
4052                    }
4053                },
4054                MoreExecutors.directExecutor());
4055    }
4056
4057    public ListenableFuture<Void> checkForAvatar(final Account account) {
4058        final var connection = account.getXmppConnection();
4059        return connection
4060                .getManager(AvatarManager.class)
4061                .fetchAndStore(account.getJid().asBareJid());
4062    }
4063
4064    public void notifyAccountAvatarHasChanged(final Account account) {
4065        final XmppConnection connection = account.getXmppConnection();
4066        // this was bookmark conversion for a bit which doesn't make sense
4067        if (connection.getManager(AvatarManager.class).hasPepToVCardConversion()) {
4068            Log.d(
4069                    Config.LOGTAG,
4070                    account.getJid().asBareJid()
4071                            + ": avatar changed. resending presence to online group chats");
4072            for (Conversation conversation : conversations) {
4073                if (conversation.getAccount() == account
4074                        && conversation.getMode() == Conversational.MODE_MULTI) {
4075                    connection.getManager(MultiUserChatManager.class).resendPresence(conversation);
4076                }
4077            }
4078        }
4079    }
4080
4081    public void fetchVcard4(Account account, final Contact contact, final Consumer<Element> callback) {
4082        final var packet = this.mIqGenerator.retrieveVcard4(contact.getJid());
4083        sendIqPacket(account, packet, (result) -> {
4084            if (result.getType() == Iq.Type.RESULT) {
4085                final Element item = IqParser.getItem(result);
4086                if (item != null) {
4087                    final Element vcard4 = item.findChild("vcard", Namespace.VCARD4);
4088                    if (vcard4 != null) {
4089                        if (callback != null) {
4090                            callback.accept(vcard4);
4091                        }
4092                        return;
4093                    }
4094                }
4095            } else {
4096                Element error = result.findChild("error");
4097                if (error == null) {
4098                    Log.d(Config.LOGTAG, "fetchVcard4 (server error)");
4099                } else {
4100                    Log.d(Config.LOGTAG, "fetchVcard4 " + error.toString());
4101                }
4102            }
4103            if (callback != null) {
4104                callback.accept(null);
4105            }
4106
4107        });
4108    }
4109
4110    public void updateConversation(final Conversation conversation) {
4111        mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation));
4112    }
4113
4114    public void reconnectAccount(
4115            final Account account, final boolean force, final boolean interactive) {
4116        synchronized (account) {
4117            final XmppConnection connection = account.getXmppConnection();
4118            final boolean hasInternet = hasInternetConnection();
4119            if (account.isConnectionEnabled() && hasInternet) {
4120                if (!force) {
4121                    disconnect(account, false);
4122                }
4123                Thread thread = new Thread(connection);
4124                connection.setInteractive(interactive);
4125                connection.prepareNewConnection();
4126                connection.interrupt();
4127                thread.start();
4128                scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
4129            } else {
4130                disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
4131                connection.getManager(RosterManager.class).clearPresences();
4132                connection.resetEverything();
4133                final AxolotlService axolotlService = account.getAxolotlService();
4134                if (axolotlService != null) {
4135                    axolotlService.resetBrokenness();
4136                }
4137                if (!hasInternet) {
4138                    // TODO should this go via XmppConnection.setStatusAndTriggerProcessor()?
4139                    account.setStatus(Account.State.NO_INTERNET);
4140                }
4141            }
4142        }
4143    }
4144
4145    public void reconnectAccountInBackground(final Account account) {
4146        new Thread(() -> reconnectAccount(account, false, true)).start();
4147    }
4148
4149    public void invite(final Conversation conversation, final Jid contact) {
4150        final var account = conversation.getAccount();
4151        account.getXmppConnection()
4152                .getManager(MultiUserChatManager.class)
4153                .invite(conversation, contact);
4154    }
4155
4156    public void directInvite(Conversation conversation, Jid jid) {
4157        final var account = conversation.getAccount();
4158        account.getXmppConnection()
4159                .getManager(MultiUserChatManager.class)
4160                .directInvite(conversation, jid);
4161    }
4162
4163    public void resetSendingToWaiting(Account account) {
4164        for (Conversation conversation : getConversations()) {
4165            if (conversation.getAccount() == account) {
4166                conversation.findUnsentTextMessages(
4167                        message -> markMessage(message, Message.STATUS_WAITING));
4168            }
4169        }
4170    }
4171
4172    public Message markMessage(
4173            final Account account, final Jid recipient, final String uuid, final int status) {
4174        return markMessage(account, recipient, uuid, status, null);
4175    }
4176
4177    public Message markMessage(
4178            final Account account,
4179            final Jid recipient,
4180            final String uuid,
4181            final int status,
4182            String errorMessage) {
4183        if (uuid == null) {
4184            return null;
4185        }
4186        for (Conversation conversation : getConversations()) {
4187            if (conversation.getJid().asBareJid().equals(recipient)
4188                    && conversation.getAccount() == account) {
4189                final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
4190                if (message != null) {
4191                    markMessage(message, status, errorMessage);
4192                }
4193                return message;
4194            }
4195        }
4196        return null;
4197    }
4198
4199    public boolean markMessage(
4200            final Conversation conversation,
4201            final String uuid,
4202            final int status,
4203            final String serverMessageId) {
4204        return markMessage(conversation, uuid, status, serverMessageId, null, null, null, null, null);
4205    }
4206
4207    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) {
4208        if (uuid == null) {
4209            return false;
4210        } else {
4211            final Message message = conversation.findSentMessageWithUuid(uuid);
4212            if (message != null) {
4213                if (message.getServerMsgId() == null) {
4214                    message.setServerMsgId(serverMessageId);
4215                }
4216                if (message.getEncryption() == Message.ENCRYPTION_NONE && (body != null || html != null || subject != null || thread != null || attachments != null)) {
4217                    message.setBody(body.content);
4218                    if (body.count > 1) {
4219                        message.setBodyLanguage(body.language);
4220                    }
4221                    message.setHtml(html);
4222                    message.setSubject(subject);
4223                    message.setThread(thread);
4224                    if (attachments != null && attachments.isEmpty()) {
4225                        message.setRelativeFilePath(null);
4226                        message.resetFileParams();
4227                    }
4228                    markMessage(message, status, null, true);
4229                } else {
4230                    markMessage(message, status);
4231                }
4232                return true;
4233            } else {
4234                return false;
4235            }
4236        }
4237    }
4238
4239    public void markMessage(Message message, int status) {
4240        markMessage(message, status, null);
4241    }
4242
4243    public void markMessage(final Message message, final int status, final String errorMessage) {
4244        markMessage(message, status, errorMessage, false);
4245    }
4246
4247    public void markMessage(
4248            final Message message,
4249            final int status,
4250            final String errorMessage,
4251            final boolean includeBody) {
4252        final int oldStatus = message.getStatus();
4253        if (status == Message.STATUS_SEND_FAILED
4254                && (oldStatus == Message.STATUS_SEND_RECEIVED
4255                        || oldStatus == Message.STATUS_SEND_DISPLAYED)) {
4256            return;
4257        }
4258        if (status == Message.STATUS_SEND_RECEIVED && oldStatus == Message.STATUS_SEND_DISPLAYED) {
4259            return;
4260        }
4261        message.setErrorMessage(errorMessage);
4262        message.setStatus(status);
4263        databaseBackend.updateMessage(message, includeBody);
4264        updateConversationUi();
4265        if (oldStatus != status && status == Message.STATUS_SEND_FAILED) {
4266            mNotificationService.pushFailedDelivery(message);
4267        }
4268    }
4269
4270    public SharedPreferences getPreferences() {
4271        return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
4272    }
4273
4274    public long getAutomaticMessageDeletionDate() {
4275        final long timeout =
4276                getLongPreference(
4277                        AppSettings.AUTOMATIC_MESSAGE_DELETION,
4278                        R.integer.automatic_message_deletion);
4279        return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000));
4280    }
4281
4282    public long getLongPreference(String name, @IntegerRes int res) {
4283        long defaultValue = getResources().getInteger(res);
4284        try {
4285            return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue)));
4286        } catch (NumberFormatException e) {
4287            return defaultValue;
4288        }
4289    }
4290
4291    public boolean getBooleanPreference(String name, int res) {
4292        return getPreferences().getBoolean(name, getResources().getBoolean(res));
4293    }
4294
4295    public String getStringPreference(String name, int res) {
4296        return getPreferences().getString(name, getResources().getString(res));
4297    }
4298
4299    public boolean confirmMessages() {
4300        return appSettings.isConfirmMessages();
4301    }
4302
4303    public boolean allowMessageCorrection() {
4304        return appSettings.isAllowMessageCorrection();
4305    }
4306
4307    public boolean useTorToConnect() {
4308        return appSettings.isUseTor();
4309    }
4310
4311    public int unreadCount() {
4312        int count = 0;
4313        for (Conversation conversation : getConversations()) {
4314            count += conversation.unreadCount(this);
4315        }
4316        return count;
4317    }
4318
4319    private <T> List<T> threadSafeList(Set<T> set) {
4320        synchronized (LISTENER_LOCK) {
4321            return set.isEmpty() ? Collections.emptyList() : new ArrayList<>(set);
4322        }
4323    }
4324
4325    public void showErrorToastInUi(int resId) {
4326        for (OnShowErrorToast listener : threadSafeList(this.mOnShowErrorToasts)) {
4327            listener.onShowErrorToast(resId);
4328        }
4329    }
4330
4331    public void updateConversationUi() {
4332        updateConversationUi(false);
4333    }
4334
4335    public void updateConversationUi(boolean newCaps) {
4336        for (OnConversationUpdate listener : threadSafeList(this.mOnConversationUpdates)) {
4337            listener.onConversationUpdate(newCaps);
4338        }
4339    }
4340
4341    public void notifyJingleRtpConnectionUpdate(
4342            final Account account,
4343            final Jid with,
4344            final String sessionId,
4345            final RtpEndUserState state) {
4346        for (OnJingleRtpConnectionUpdate listener :
4347                threadSafeList(this.onJingleRtpConnectionUpdate)) {
4348            listener.onJingleRtpConnectionUpdate(account, with, sessionId, state);
4349        }
4350    }
4351
4352    public void notifyJingleRtpConnectionUpdate(
4353            CallIntegration.AudioDevice selectedAudioDevice,
4354            Set<CallIntegration.AudioDevice> availableAudioDevices) {
4355        for (OnJingleRtpConnectionUpdate listener :
4356                threadSafeList(this.onJingleRtpConnectionUpdate)) {
4357            listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
4358        }
4359    }
4360
4361    public void updateAccountUi() {
4362        for (final OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) {
4363            listener.onAccountUpdate();
4364        }
4365    }
4366
4367    public void updateRosterUi(final UpdateRosterReason reason) {
4368        if (reason == UpdateRosterReason.PRESENCE) throw new IllegalArgumentException("PRESENCE must also come with a contact");
4369        updateRosterUi(reason, null);
4370    }
4371
4372    public void updateRosterUi(final UpdateRosterReason reason, final Contact contact) {
4373        for (OnRosterUpdate listener : threadSafeList(this.mOnRosterUpdates)) {
4374            listener.onRosterUpdate(reason, contact);
4375        }
4376    }
4377
4378    public boolean displayCaptchaRequest(
4379            final Account account,
4380            final im.conversations.android.xmpp.model.data.Data data,
4381            final Bitmap captcha) {
4382        if (mOnCaptchaRequested.isEmpty()) {
4383            return false;
4384        }
4385        final var metrics = getApplicationContext().getResources().getDisplayMetrics();
4386        Bitmap scaled =
4387                Bitmap.createScaledBitmap(
4388                        captcha,
4389                        (int) (captcha.getWidth() * metrics.scaledDensity),
4390                        (int) (captcha.getHeight() * metrics.scaledDensity),
4391                        false);
4392        for (final OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
4393            listener.onCaptchaRequested(account, data, scaled);
4394        }
4395        return true;
4396    }
4397
4398    public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
4399        for (OnUpdateBlocklist listener : threadSafeList(this.mOnUpdateBlocklist)) {
4400            listener.OnUpdateBlocklist(status);
4401        }
4402    }
4403
4404    public void updateMucRosterUi() {
4405        for (OnMucRosterUpdate listener : threadSafeList(this.mOnMucRosterUpdate)) {
4406            listener.onMucRosterUpdate();
4407        }
4408    }
4409
4410    public void keyStatusUpdated(AxolotlService.FetchStatus report) {
4411        for (OnKeyStatusUpdated listener : threadSafeList(this.mOnKeyStatusUpdated)) {
4412            listener.onKeyStatusUpdated(report);
4413        }
4414    }
4415
4416    public Account findAccountByJid(final Jid jid) {
4417        for (final Account account : this.accounts) {
4418            if (account.getJid().asBareJid().equals(jid.asBareJid())) {
4419                return account;
4420            }
4421        }
4422        return null;
4423    }
4424
4425    public Account findAccountByUuid(final String uuid) {
4426        for (Account account : this.accounts) {
4427            if (account.getUuid().equals(uuid)) {
4428                return account;
4429            }
4430        }
4431        return null;
4432    }
4433
4434    public Conversation findConversationByUuid(String uuid) {
4435        for (Conversation conversation : getConversations()) {
4436            if (conversation.getUuid().equals(uuid)) {
4437                return conversation;
4438            }
4439        }
4440        return null;
4441    }
4442
4443    public Conversation findUniqueConversationByJid(XmppUri xmppUri) {
4444        List<Conversation> findings = new ArrayList<>();
4445        for (Conversation c : getConversations()) {
4446            if (c.getAccount().isEnabled()
4447                    && c.getJid().asBareJid().equals(xmppUri.getJid().asBareJid())
4448                    && ((c.getMode() == Conversational.MODE_MULTI)
4449                            == xmppUri.isAction(XmppUri.ACTION_JOIN))) {
4450                findings.add(c);
4451            }
4452        }
4453        return findings.size() == 1 ? findings.get(0) : null;
4454    }
4455
4456    public boolean markRead(final Conversation conversation, boolean dismiss) {
4457        return markRead(conversation, null, dismiss).size() > 0;
4458    }
4459
4460    public void markRead(final Conversation conversation) {
4461        markRead(conversation, null, true);
4462    }
4463
4464    public List<Message> markRead(
4465            final Conversation conversation, String upToUuid, boolean dismiss) {
4466        if (dismiss) {
4467            mNotificationService.clear(conversation);
4468        }
4469        final List<Message> readMessages = conversation.markRead(upToUuid);
4470        if (readMessages.size() > 0) {
4471            Runnable runnable =
4472                    () -> {
4473                        for (Message message : readMessages) {
4474                            databaseBackend.updateMessage(message, false);
4475                        }
4476                    };
4477            mDatabaseWriterExecutor.execute(runnable);
4478            updateConversationUi();
4479            updateUnreadCountBadge();
4480            return readMessages;
4481        } else {
4482            return readMessages;
4483        }
4484    }
4485
4486    public void markNotificationDismissed(final List<Message> messages) {
4487        Runnable runnable = () -> {
4488            for (final var message : messages) {
4489                message.markNotificationDismissed();
4490                databaseBackend.updateMessage(message, false);
4491            }
4492        };
4493        mDatabaseWriterExecutor.execute(runnable);
4494    }
4495
4496    public synchronized void updateUnreadCountBadge() {
4497        int count = unreadCount();
4498        if (unreadCount != count) {
4499            Log.d(Config.LOGTAG, "update unread count to " + count);
4500            if (count > 0) {
4501                ShortcutBadger.applyCount(getApplicationContext(), count);
4502            } else {
4503                ShortcutBadger.removeCount(getApplicationContext());
4504            }
4505            unreadCount = count;
4506        }
4507    }
4508
4509    public void sendReadMarker(final Conversation conversation, final String upToUuid) {
4510        final boolean isPrivateAndNonAnonymousMuc =
4511                conversation.getMode() == Conversation.MODE_MULTI
4512                        && conversation.isPrivateAndNonAnonymous();
4513        final List<Message> readMessages = this.markRead(conversation, upToUuid, true);
4514        if (readMessages.isEmpty()) {
4515            return;
4516        }
4517        final var account = conversation.getAccount();
4518        final var connection = account.getXmppConnection();
4519        updateConversationUi();
4520        final var last =
4521                Iterables.getLast(
4522                        Collections2.filter(
4523                                readMessages,
4524                                m ->
4525                                        !m.isPrivateMessage()
4526                                                && m.getStatus() == Message.STATUS_RECEIVED),
4527                        null);
4528        if (last == null) {
4529            return;
4530        }
4531
4532        final boolean sendDisplayedMarker =
4533                confirmMessages()
4534                        && (last.trusted() || isPrivateAndNonAnonymousMuc)
4535                        && last.getRemoteMsgId() != null
4536                        && (last.markable || isPrivateAndNonAnonymousMuc);
4537        final boolean serverAssist =
4538                connection != null && connection.getFeatures().mdsServerAssist();
4539
4540        final String stanzaId = last.getServerMsgId();
4541
4542        if (sendDisplayedMarker && serverAssist) {
4543            final var mdsDisplayed =
4544                    MessageDisplayedSynchronizationManager.displayed(stanzaId, conversation);
4545            final var packet = mMessageGenerator.confirm(last);
4546            packet.addChild(mdsDisplayed);
4547            if (!last.isPrivateMessage()) {
4548                packet.setTo(packet.getTo().asBareJid());
4549            }
4550            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server assisted " + packet);
4551            this.sendMessagePacket(account, packet);
4552        } else {
4553            publishMds(last);
4554            // read markers will be sent after MDS to flush the CSI stanza queue
4555            if (sendDisplayedMarker) {
4556                Log.d(
4557                        Config.LOGTAG,
4558                        conversation.getAccount().getJid().asBareJid()
4559                                + ": sending displayed marker to "
4560                                + last.getCounterpart().toString());
4561                final var packet = mMessageGenerator.confirm(last);
4562                this.sendMessagePacket(account, packet);
4563            }
4564        }
4565    }
4566
4567    private void publishMds(@Nullable final Message message) {
4568        final String stanzaId = message == null ? null : message.getServerMsgId();
4569        if (Strings.isNullOrEmpty(stanzaId)) {
4570            return;
4571        }
4572        final Conversation conversation;
4573        final var conversational = message.getConversation();
4574        if (conversational instanceof Conversation c) {
4575            conversation = c;
4576        } else {
4577            return;
4578        }
4579        final var account = conversation.getAccount();
4580        final var connection = account.getXmppConnection();
4581        if (connection == null || !connection.getFeatures().mds()) {
4582            return;
4583        }
4584        final Jid itemId;
4585        if (message.isPrivateMessage()) {
4586            itemId = message.getCounterpart();
4587        } else {
4588            itemId = conversation.getJid().asBareJid();
4589        }
4590        Log.d(Config.LOGTAG, "publishing mds for " + itemId + "/" + stanzaId);
4591        final var displayed =
4592                MessageDisplayedSynchronizationManager.displayed(stanzaId, conversation);
4593        connection
4594                .getManager(MessageDisplayedSynchronizationManager.class)
4595                .publish(itemId, displayed);
4596    }
4597
4598    public boolean sendReactions(final Message message, final Collection<String> reactions) {
4599        if (message.isPrivateMessage()) throw new IllegalArgumentException("Reactions to PM not implemented");
4600        if (message.getConversation() instanceof Conversation conversation) {
4601            final var isPrivateMessage = message.isPrivateMessage();
4602            final Jid reactTo;
4603            final boolean typeGroupChat;
4604            final String reactToId;
4605            final Collection<Reaction> combinedReactions;
4606            final var newReactions = new HashSet<>(reactions);
4607            newReactions.removeAll(message.getAggregatedReactions().ourReactions);
4608            if (conversation.getMode() == Conversational.MODE_MULTI && !isPrivateMessage) {
4609                final var mucOptions = conversation.getMucOptions();
4610                if (!mucOptions.participating()) {
4611                    Log.e(Config.LOGTAG, "not participating in MUC");
4612                    return false;
4613                }
4614                final var self = mucOptions.getSelf();
4615                final String occupantId = self.getOccupantId();
4616                if (Strings.isNullOrEmpty(occupantId)) {
4617                    Log.e(Config.LOGTAG, "occupant id not found for reaction in MUC");
4618                    return false;
4619                }
4620                final var existingRaw =
4621                        ImmutableSet.copyOf(
4622                                Collections2.transform(message.getReactions(), r -> r.reaction));
4623                final var reactionsAsExistingVariants =
4624                        ImmutableSet.copyOf(
4625                                Collections2.transform(
4626                                        reactions, r -> Emoticons.existingVariant(r, existingRaw)));
4627                if (!reactions.equals(reactionsAsExistingVariants)) {
4628                    Log.d(Config.LOGTAG, "modified reactions to existing variants");
4629                }
4630                reactToId = message.getServerMsgId();
4631                reactTo = conversation.getJid().asBareJid();
4632                typeGroupChat = true;
4633                combinedReactions =
4634                        Reaction.withMine(
4635                                message.getReactions(),
4636                                reactionsAsExistingVariants,
4637                                false,
4638                                self.getFullJid(),
4639                                conversation.getAccount().getJid(),
4640                                occupantId,
4641                                null);
4642            } else {
4643                if (message.isCarbon() || message.getStatus() == Message.STATUS_RECEIVED) {
4644                    reactToId = message.getRemoteMsgId();
4645                } else {
4646                    reactToId = message.getUuid();
4647                }
4648                typeGroupChat = false;
4649                if (isPrivateMessage) {
4650                    reactTo = message.getCounterpart();
4651                } else {
4652                    reactTo = conversation.getJid().asBareJid();
4653                }
4654                combinedReactions =
4655                        Reaction.withFrom(
4656                                message.getReactions(),
4657                                reactions,
4658                                false,
4659                                conversation.getAccount().getJid(),
4660                                null);
4661            }
4662            if (reactTo == null || Strings.isNullOrEmpty(reactToId)) {
4663                Log.e(Config.LOGTAG, "could not find id to react to");
4664                return false;
4665            }
4666
4667            final var packet =
4668                    mMessageGenerator.reaction(reactTo, typeGroupChat, message, reactToId, reactions);
4669
4670            final var quote = QuoteHelper.quote(MessageUtils.prepareQuote(message, 1, 2)) + "\n\n";
4671            final var body  = quote + String.join(" ", newReactions);
4672            if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && newReactions.size() > 0) {
4673                FILE_ATTACHMENT_EXECUTOR.execute(() -> {
4674                    XmppAxolotlMessage axolotlMessage = conversation.getAccount().getAxolotlService().encrypt(body, conversation);
4675                    packet.setAxolotlMessage(axolotlMessage.toElement());
4676                    packet.addChild("encryption", "urn:xmpp:eme:0")
4677                        .setAttribute("name", "OMEMO")
4678                        .setAttribute("namespace", AxolotlService.PEP_PREFIX);
4679                    sendMessagePacket(conversation.getAccount(), packet);
4680                    message.setReactions(combinedReactions);
4681                    updateMessage(message, false);
4682                });
4683            } else if (conversation.getNextEncryption() == Message.ENCRYPTION_NONE || newReactions.size() < 1) {
4684                if (newReactions.size() > 0) {
4685                    packet.setBody(body);
4686
4687                    packet.addChild("reply", "urn:xmpp:reply:0")
4688                        .setAttribute("to", message.getCounterpart())
4689                        .setAttribute("id", reactToId);
4690                    final var replyFallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reply:0");
4691                    replyFallback.addChild("body", "urn:xmpp:fallback:0")
4692                        .setAttribute("start", "0")
4693                        .setAttribute("end", "" + quote.codePointCount(0, quote.length()));
4694
4695                    final var fallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reactions:0");
4696                    fallback.addChild("body", "urn:xmpp:fallback:0");
4697                }
4698
4699                sendMessagePacket(conversation.getAccount(), packet);
4700                message.setReactions(combinedReactions);
4701                updateMessage(message, false);
4702            }
4703
4704            return true;
4705        } else {
4706            return false;
4707        }
4708    }
4709
4710    public MemorizingTrustManager getMemorizingTrustManager() {
4711        return this.mMemorizingTrustManager;
4712    }
4713
4714    public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
4715        this.mMemorizingTrustManager = trustManager;
4716    }
4717
4718    public void updateMemorizingTrustManager() {
4719        final MemorizingTrustManager trustManager;
4720        if (appSettings.isTrustSystemCAStore()) {
4721            trustManager = new MemorizingTrustManager(getApplicationContext());
4722        } else {
4723            trustManager = new MemorizingTrustManager(getApplicationContext(), null);
4724        }
4725        setMemorizingTrustManager(trustManager);
4726    }
4727
4728    public LruCache<String, Drawable> getDrawableCache() {
4729        return this.mDrawableCache;
4730    }
4731
4732    public Collection<String> getKnownHosts() {
4733        final Set<String> hosts = new HashSet<>();
4734        for (final Account account : getAccounts()) {
4735            hosts.add(account.getServer());
4736            for (final Contact contact : account.getRoster().getContacts()) {
4737                if (contact.showInRoster()) {
4738                    final String server = contact.getServer();
4739                    if (server != null) {
4740                        hosts.add(server);
4741                    }
4742                }
4743            }
4744        }
4745        if (Config.QUICKSY_DOMAIN != null) {
4746            hosts.remove(
4747                    Config.QUICKSY_DOMAIN
4748                            .toString()); // we only want to show this when we type a e164
4749            // number
4750        }
4751        if (Config.MAGIC_CREATE_DOMAIN != null) {
4752            hosts.add(Config.MAGIC_CREATE_DOMAIN);
4753        }
4754        hosts.add("chat.above.im");
4755        return hosts;
4756    }
4757
4758    public Collection<String> getKnownConferenceHosts() {
4759        final var builder = new ImmutableSet.Builder<Jid>();
4760        for (final Account account : accounts) {
4761            final var connection = account.getXmppConnection();
4762            builder.addAll(connection.getManager(MultiUserChatManager.class).getServices());
4763            for (final var bookmark : account.getBookmarks()) {
4764                final Jid jid = bookmark.getJid();
4765                final Jid domain = jid == null ? null : jid.getDomain();
4766                if (domain == null) {
4767                    continue;
4768                }
4769                builder.add(domain);
4770            }
4771        }
4772        return Collections2.transform(builder.build(), Jid::toString);
4773    }
4774
4775    public void sendMessagePacket(
4776            final Account account,
4777            final im.conversations.android.xmpp.model.stanza.Message packet) {
4778        final XmppConnection connection = account.getXmppConnection();
4779        if (connection != null) {
4780            connection.sendMessagePacket(packet);
4781        }
4782    }
4783
4784    public ListenableFuture<Iq> sendIqPacket(final Account account, final Iq request) {
4785        final XmppConnection connection = account.getXmppConnection();
4786        if (connection == null) {
4787            return Futures.immediateFailedFuture(new TimeoutException());
4788        }
4789        return connection.sendIqPacket(request);
4790    }
4791
4792    public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback) {
4793        sendIqPacket(account, packet, callback, null);
4794    }
4795
4796    public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback, Long timeout) {
4797        final XmppConnection connection = account.getXmppConnection();
4798        if (connection != null) {
4799            connection.sendIqPacket(packet, callback, timeout);
4800        } else if (callback != null) {
4801            callback.accept(Iq.TIMEOUT);
4802        }
4803    }
4804
4805    private void deactivateGracePeriod() {
4806        for (Account account : getAccounts()) {
4807            account.deactivateGracePeriod();
4808        }
4809    }
4810
4811    public void refreshAllPresences() {
4812        final boolean includeIdleTimestamp =
4813                checkListeners() && appSettings.isBroadcastLastActivity();
4814        for (final var account : getAccounts()) {
4815            if (account.isConnectionEnabled()) {
4816                account.getXmppConnection()
4817                        .getManager(PresenceManager.class)
4818                        .available(includeIdleTimestamp);
4819            }
4820        }
4821    }
4822
4823    private void refreshAllFcmTokens() {
4824        for (Account account : getAccounts()) {
4825            if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
4826                mPushManagementService.registerPushTokenOnServer(account);
4827            }
4828        }
4829    }
4830
4831    public MessageGenerator getMessageGenerator() {
4832        return this.mMessageGenerator;
4833    }
4834
4835    public IqGenerator getIqGenerator() {
4836        return this.mIqGenerator;
4837    }
4838
4839    public JingleConnectionManager getJingleConnectionManager() {
4840        return this.mJingleConnectionManager;
4841    }
4842
4843    public boolean hasJingleRtpConnection(final Account account) {
4844        return this.mJingleConnectionManager.hasJingleRtpConnection(account);
4845    }
4846
4847    public MessageArchiveService getMessageArchiveService() {
4848        return this.mMessageArchiveService;
4849    }
4850
4851    public QuickConversationsService getQuickConversationsService() {
4852        return this.mQuickConversationsService;
4853    }
4854
4855    public List<Contact> findContacts(Jid jid, String accountJid) {
4856        ArrayList<Contact> contacts = new ArrayList<>();
4857        for (Account account : getAccounts()) {
4858            if ((account.isEnabled() || accountJid != null)
4859                    && (accountJid == null
4860                            || accountJid.equals(account.getJid().asBareJid().toString()))) {
4861                Contact contact = account.getRoster().getContactFromContactList(jid);
4862                if (contact != null) {
4863                    contacts.add(contact);
4864                }
4865            }
4866        }
4867        return contacts;
4868    }
4869
4870    public Conversation findFirstMuc(Jid jid) {
4871        return findFirstMuc(jid, null);
4872    }
4873
4874    public Conversation findFirstMuc(Jid jid, String accountJid) {
4875        for (Conversation conversation : getConversations()) {
4876            if ((conversation.getAccount().isEnabled() || accountJid != null)
4877                    && (accountJid == null || accountJid.equals(conversation.getAccount().getJid().asBareJid().toString()))
4878                    && conversation.getJid().asBareJid().equals(jid.asBareJid()) && conversation.getMode() == Conversation.MODE_MULTI) {
4879                return conversation;
4880            }
4881        }
4882        return null;
4883    }
4884
4885    public NotificationService getNotificationService() {
4886        return this.mNotificationService;
4887    }
4888
4889    public HttpConnectionManager getHttpConnectionManager() {
4890        return this.mHttpConnectionManager;
4891    }
4892
4893    public void resendFailedMessages(final Message message, final boolean forceP2P) {
4894        message.setTime(System.currentTimeMillis());
4895        markMessage(message, Message.STATUS_WAITING);
4896        this.sendMessage(message, true, false, false, forceP2P, null);
4897        if (message.getConversation() instanceof Conversation c) {
4898            c.sort();
4899        }
4900        updateConversationUi();
4901    }
4902
4903    public void clearConversationHistory(final Conversation conversation) {
4904        final long clearDate;
4905        final String reference;
4906        if (conversation.countMessages() > 0) {
4907            Message latestMessage = conversation.getLatestMessage();
4908            clearDate = latestMessage.getTimeSent() + 1000;
4909            reference = latestMessage.getServerMsgId();
4910        } else {
4911            clearDate = System.currentTimeMillis();
4912            reference = null;
4913        }
4914        conversation.clearMessages();
4915        conversation.setHasMessagesLeftOnServer(false); // avoid messages getting loaded through mam
4916        conversation.setLastClearHistory(clearDate, reference);
4917        Runnable runnable =
4918                () -> {
4919                    databaseBackend.deleteMessagesInConversation(conversation);
4920                    databaseBackend.updateConversation(conversation);
4921                };
4922        mDatabaseWriterExecutor.execute(runnable);
4923    }
4924
4925    public boolean sendBlockRequest(
4926            final Blockable blockable, final boolean reportSpam, final String serverMsgId) {
4927        final var account = blockable.getAccount();
4928        final var connection = account.getXmppConnection();
4929        return connection
4930                .getManager(BlockingManager.class)
4931                .block(blockable, reportSpam, serverMsgId);
4932    }
4933
4934    public boolean removeBlockedConversations(final Account account, final Jid blockedJid) {
4935        boolean removed = false;
4936        synchronized (this.conversations) {
4937            boolean domainJid = blockedJid.getLocal() == null;
4938            for (Conversation conversation : this.conversations) {
4939                boolean jidMatches =
4940                        (domainJid
4941                                        && blockedJid
4942                                                .getDomain()
4943                                                .equals(conversation.getJid().getDomain()))
4944                                || blockedJid.equals(conversation.getJid().asBareJid());
4945                if (conversation.getAccount() == account
4946                        && conversation.getMode() == Conversation.MODE_SINGLE
4947                        && jidMatches) {
4948                    this.conversations.remove(conversation);
4949                    getMessageArchiveService().kill(conversation);
4950                    markRead(conversation);
4951                    conversation.setStatus(Conversation.STATUS_ARCHIVED);
4952                    Log.d(
4953                            Config.LOGTAG,
4954                            account.getJid().asBareJid()
4955                                    + ": archiving conversation "
4956                                    + conversation.getJid().asBareJid()
4957                                    + " because jid was blocked");
4958                    updateConversation(conversation);
4959                    removed = true;
4960                }
4961            }
4962        }
4963        return removed;
4964    }
4965
4966    public void sendUnblockRequest(final Blockable blockable) {
4967        final var account = blockable.getAccount();
4968        final var connection = account.getXmppConnection();
4969        connection.getManager(BlockingManager.class).unblock(blockable);
4970    }
4971
4972    public void publishDisplayName(final Account account) {
4973        final var connection = account.getXmppConnection();
4974        final String displayName = account.getDisplayName();
4975        mAvatarService.clear(account);
4976        final var future = connection.getManager(NickManager.class).publish(displayName);
4977        Futures.addCallback(
4978                future,
4979                new FutureCallback<Void>() {
4980                    @Override
4981                    public void onSuccess(Void result) {
4982                        Log.d(
4983                                Config.LOGTAG,
4984                                account.getJid().asBareJid() + ": published User Nick");
4985                    }
4986
4987                    @Override
4988                    public void onFailure(@NonNull Throwable t) {
4989                        Log.d(Config.LOGTAG, "could not publish User Nick", t);
4990                    }
4991                },
4992                MoreExecutors.directExecutor());
4993    }
4994
4995    public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) {
4996        final var request = new Iq(input == null ? Iq.Type.GET : Iq.Type.SET);
4997        request.setTo(jid);
4998        Element query = request.addChild("query");
4999        query.setAttribute("xmlns", "jabber:iq:gateway");
5000        if (input != null) {
5001            Element prompt = query.addChild("prompt");
5002            prompt.setContent(input);
5003        }
5004        sendIqPacket(account, request, packet -> {
5005            if (packet.getType() == Iq.Type.RESULT) {
5006                callback.onGatewayResult(packet.findChild("query").findChildContent(input == null ? "prompt" : "jid"), null);
5007            } else {
5008                Element error = packet.findChild("error");
5009                callback.onGatewayResult(null, error == null ? null : error.findChildContent("text"));
5010            }
5011        });
5012    }
5013
5014    public void fetchMamPreferences(final Account account, final OnMamPreferencesFetched callback) {
5015        final MessageArchiveService.Version version = MessageArchiveService.Version.get(account);
5016        final Iq request = new Iq(Iq.Type.GET);
5017        request.addChild("prefs", version.namespace);
5018        sendIqPacket(
5019                account,
5020                request,
5021                (packet) -> {
5022                    final Element prefs = packet.findChild("prefs", version.namespace);
5023                    if (packet.getType() == Iq.Type.RESULT && prefs != null) {
5024                        account.setMamPrefs(prefs);
5025                        callback.onPreferencesFetched(prefs);
5026                    } else {
5027                        callback.onPreferencesFetchFailed();
5028                    }
5029                });
5030    }
5031
5032    public PushManagementService getPushManagementService() {
5033        return mPushManagementService;
5034    }
5035
5036    public void changeStatus(
5037            final Account account, final PresenceTemplate template, final String signature) {
5038        if (!template.getStatusMessage().isEmpty()) {
5039            databaseBackend.insertPresenceTemplate(template);
5040        }
5041        account.setPgpSignature(signature);
5042        account.setPresenceStatus(template.getStatus());
5043        account.setPresenceStatusMessage(template.getStatusMessage());
5044        databaseBackend.updateAccount(account);
5045        account.getXmppConnection().getManager(PresenceManager.class).available();
5046    }
5047
5048    public List<PresenceTemplate> getPresenceTemplates(Account account) {
5049        List<PresenceTemplate> templates = databaseBackend.getPresenceTemplates();
5050        for (PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) {
5051            if (!templates.contains(template)) {
5052                templates.add(0, template);
5053            }
5054        }
5055        return templates;
5056    }
5057
5058    public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
5059        boolean performedVerification = false;
5060        final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
5061        for (XmppUri.Fingerprint fp : fingerprints) {
5062            if (fp.type == XmppUri.FingerprintType.OMEMO) {
5063                String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
5064                FingerprintStatus fingerprintStatus =
5065                        axolotlService.getFingerprintTrust(fingerprint);
5066                if (fingerprintStatus != null) {
5067                    if (!fingerprintStatus.isVerified()) {
5068                        performedVerification = true;
5069                        axolotlService.setFingerprintTrust(
5070                                fingerprint, fingerprintStatus.toVerified());
5071                    }
5072                } else {
5073                    axolotlService.preVerifyFingerprint(contact, fingerprint);
5074                }
5075            }
5076        }
5077        return performedVerification;
5078    }
5079
5080    public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
5081        final AxolotlService axolotlService = account.getAxolotlService();
5082        boolean verifiedSomething = false;
5083        for (XmppUri.Fingerprint fp : fingerprints) {
5084            if (fp.type == XmppUri.FingerprintType.OMEMO) {
5085                String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
5086                Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint);
5087                FingerprintStatus fingerprintStatus =
5088                        axolotlService.getFingerprintTrust(fingerprint);
5089                if (fingerprintStatus != null) {
5090                    if (!fingerprintStatus.isVerified()) {
5091                        axolotlService.setFingerprintTrust(
5092                                fingerprint, fingerprintStatus.toVerified());
5093                        verifiedSomething = true;
5094                    }
5095                } else {
5096                    axolotlService.preVerifyFingerprint(account, fingerprint);
5097                    verifiedSomething = true;
5098                }
5099            }
5100        }
5101        return verifiedSomething;
5102    }
5103
5104    public ShortcutService getShortcutService() {
5105        return mShortcutService;
5106    }
5107
5108    public void pushMamPreferences(Account account, Element prefs) {
5109        final Iq set = new Iq(Iq.Type.SET);
5110        set.addChild(prefs);
5111        account.setMamPrefs(prefs);
5112        sendIqPacket(account, set, null);
5113    }
5114
5115    public void evictPreview(File f) {
5116        if (f == null) return;
5117
5118        if (mDrawableCache.remove(f.getAbsolutePath()) != null) {
5119            Log.d(Config.LOGTAG, "deleted cached preview");
5120        }
5121    }
5122
5123    public void evictPreview(String uuid) {
5124        if (mDrawableCache.remove(uuid) != null) {
5125            Log.d(Config.LOGTAG, "deleted cached preview");
5126        }
5127    }
5128
5129    public long getLastActivity() {
5130        return this.mLastActivity;
5131    }
5132
5133    public interface OnMamPreferencesFetched {
5134        void onPreferencesFetched(Element prefs);
5135
5136        void onPreferencesFetchFailed();
5137    }
5138
5139    public interface OnAccountCreated {
5140        void onAccountCreated(Account account);
5141
5142        void informUser(int r);
5143    }
5144
5145    public interface OnMoreMessagesLoaded {
5146        void onMoreMessagesLoaded(int count, Conversation conversation);
5147
5148        void informUser(int r);
5149    }
5150
5151    public interface OnAffiliationChanged {
5152        void onAffiliationChangedSuccessful(Jid jid);
5153
5154        void onAffiliationChangeFailed(Jid jid, int resId);
5155    }
5156
5157    public interface OnConversationUpdate {
5158        default void onConversationUpdate() { onConversationUpdate(false); }
5159        default void onConversationUpdate(boolean newCaps) { onConversationUpdate(); }
5160    }
5161
5162    public interface OnJingleRtpConnectionUpdate {
5163        void onJingleRtpConnectionUpdate(
5164                final Account account,
5165                final Jid with,
5166                final String sessionId,
5167                final RtpEndUserState state);
5168
5169        void onAudioDeviceChanged(
5170                CallIntegration.AudioDevice selectedAudioDevice,
5171                Set<CallIntegration.AudioDevice> availableAudioDevices);
5172    }
5173
5174    public interface OnAccountUpdate {
5175        void onAccountUpdate();
5176    }
5177
5178    public interface OnCaptchaRequested {
5179        void onCaptchaRequested(
5180                Account account,
5181                im.conversations.android.xmpp.model.data.Data data,
5182                Bitmap captcha);
5183    }
5184
5185    public interface OnRosterUpdate {
5186        void onRosterUpdate(final UpdateRosterReason reason, final Contact contact);
5187    }
5188
5189    public interface OnMucRosterUpdate {
5190        void onMucRosterUpdate();
5191    }
5192
5193    public interface OnConferenceConfigurationFetched {
5194        void onConferenceConfigurationFetched(Conversation conversation);
5195
5196        void onFetchFailed(Conversation conversation, String errorCondition);
5197    }
5198
5199    public interface OnConferenceJoined {
5200        void onConferenceJoined(Conversation conversation);
5201    }
5202
5203    public interface OnConfigurationPushed {
5204        void onPushSucceeded();
5205
5206        void onPushFailed();
5207    }
5208
5209    public interface OnShowErrorToast {
5210        void onShowErrorToast(int resId);
5211    }
5212
5213    public class XmppConnectionBinder extends Binder {
5214        public XmppConnectionService getService() {
5215            return XmppConnectionService.this;
5216        }
5217    }
5218
5219    private class InternalEventReceiver extends BroadcastReceiver {
5220
5221        @Override
5222        public void onReceive(final Context context, final Intent intent) {
5223            onStartCommand(intent, 0, 0);
5224        }
5225    }
5226
5227    private class RestrictedEventReceiver extends BroadcastReceiver {
5228
5229        private final Collection<String> allowedActions;
5230
5231        private RestrictedEventReceiver(final Collection<String> allowedActions) {
5232            this.allowedActions = allowedActions;
5233        }
5234
5235        @Override
5236        public void onReceive(final Context context, final Intent intent) {
5237            final String action = intent == null ? null : intent.getAction();
5238            if (allowedActions.contains(action)) {
5239                onStartCommand(intent, 0, 0);
5240            } else {
5241                Log.e(Config.LOGTAG, "restricting broadcast of event " + action);
5242            }
5243        }
5244    }
5245
5246    public static class OngoingCall {
5247        public final AbstractJingleConnection.Id id;
5248        public final Set<Media> media;
5249        public final boolean reconnecting;
5250
5251        public OngoingCall(
5252                AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
5253            this.id = id;
5254            this.media = media;
5255            this.reconnecting = reconnecting;
5256        }
5257
5258        @Override
5259        public boolean equals(Object o) {
5260            if (this == o) return true;
5261            if (o == null || getClass() != o.getClass()) return false;
5262            OngoingCall that = (OngoingCall) o;
5263            return reconnecting == that.reconnecting
5264                    && Objects.equal(id, that.id)
5265                    && Objects.equal(media, that.media);
5266        }
5267
5268        @Override
5269        public int hashCode() {
5270            return Objects.hashCode(id, media, reconnecting);
5271        }
5272    }
5273
5274    public static void toggleForegroundService(final XmppConnectionService service) {
5275        if (service == null) {
5276            return;
5277        }
5278        service.toggleForegroundService();
5279    }
5280
5281    public static void toggleForegroundService(final ConversationsActivity activity) {
5282        if (activity == null) {
5283            return;
5284        }
5285        toggleForegroundService(activity.xmppConnectionService);
5286    }
5287
5288    public static class BlockedMediaException extends Exception { }
5289
5290    public static enum UpdateRosterReason {
5291        INIT,
5292        AVATAR,
5293        PUSH,
5294        PRESENCE
5295    }
5296}