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