XmppConnectionService.java

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