XmppConnectionService.java

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