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