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), false);
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 jumpToMessage(final Conversation conversation, final String uuid, JumpToMessageListener listener) {
2602        final Runnable runnable = () -> {
2603            List<Message> messages = databaseBackend.getMessagesNearUuid(conversation, 30, uuid);
2604            if (messages != null && !messages.isEmpty()) {
2605                conversation.jumpToHistoryPart(messages);
2606                listener.onSuccess();
2607            } else {
2608                listener.onNotFound();
2609            }
2610        };
2611
2612        mDatabaseReaderExecutor.execute(runnable);
2613    }
2614
2615    public void loadMoreMessages(
2616            final Conversation conversation,
2617            final long timestamp,
2618            boolean isForward,
2619            final OnMoreMessagesLoaded callback) {
2620        if (XmppConnectionService.this
2621                .getMessageArchiveService()
2622                .queryInProgress(conversation, callback)) {
2623            return;
2624        } else if (timestamp == 0) {
2625            return;
2626        }
2627        Log.d(
2628                Config.LOGTAG,
2629                "load more messages for "
2630                        + conversation.getName()
2631                        + " prior to "
2632                        + MessageGenerator.getTimestamp(timestamp));
2633
2634        if (isForward) {
2635            Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " after " + MessageGenerator.getTimestamp(timestamp));
2636        } else {
2637            Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp));
2638        }
2639
2640        final Runnable runnable =
2641                () -> {
2642                    final Account account = conversation.getAccount();
2643                    List<Message> messages = databaseBackend.getMessages(conversation, Config.PAGE_SIZE, timestamp, isForward);
2644                    if (messages.size() > 0) {
2645                        if (isForward) {
2646                            conversation.addAll(-1, messages, true);
2647                        } else {
2648                            conversation.addAll(0, messages, true);
2649                        }
2650                        callback.onMoreMessagesLoaded(messages.size(), conversation);
2651                    } else if (!isForward &&
2652                            conversation.hasMessagesLeftOnServer()
2653                            && account.isOnlineAndConnected()
2654                            && conversation.getLastClearHistory().getTimestamp() == 0) {
2655                        final boolean mamAvailable;
2656                        if (conversation.getMode() == Conversation.MODE_SINGLE) {
2657                            mamAvailable =
2658                                    account.getXmppConnection().getFeatures().mam()
2659                                            && !conversation.getContact().isBlocked();
2660                        } else {
2661                            mamAvailable = conversation.getMucOptions().mamSupport();
2662                        }
2663                        if (mamAvailable) {
2664                            MessageArchiveService.Query query =
2665                                    getMessageArchiveService()
2666                                            .query(
2667                                                    conversation,
2668                                                    new MamReference(0),
2669                                                    timestamp,
2670                                                    false);
2671                            if (query != null) {
2672                                query.setCallback(callback);
2673                                callback.informUser(R.string.fetching_history_from_server);
2674                            } else {
2675                                callback.informUser(R.string.not_fetching_history_retention_period);
2676                            }
2677                        }
2678                    }
2679                };
2680        mDatabaseReaderExecutor.execute(runnable);
2681    }
2682
2683    public List<Account> getAccounts() {
2684        return this.accounts;
2685    }
2686
2687    /**
2688     * This will find all conferences with the contact as member and also the conference that is the
2689     * contact (that 'fake' contact is used to store the avatar)
2690     */
2691    public List<Conversation> findAllConferencesWith(Contact contact) {
2692        final ArrayList<Conversation> results = new ArrayList<>();
2693        for (final Conversation c : conversations) {
2694            if (c.getMode() != Conversation.MODE_MULTI) {
2695                continue;
2696            }
2697            final MucOptions mucOptions = c.getMucOptions();
2698            if (c.getJid().asBareJid().equals(contact.getJid().asBareJid())
2699                    || (mucOptions != null && mucOptions.isContactInRoom(contact))) {
2700                results.add(c);
2701            }
2702        }
2703        return results;
2704    }
2705
2706    public Conversation find(final Contact contact) {
2707        for (final Conversation conversation : this.conversations) {
2708            if (conversation.getContact() == contact) {
2709                return conversation;
2710            }
2711        }
2712        return null;
2713    }
2714
2715    public Conversation find(
2716            final Iterable<Conversation> haystack, final Account account, final Jid jid) {
2717        if (jid == null) {
2718            return null;
2719        }
2720        for (final Conversation conversation : haystack) {
2721            if ((account == null || conversation.getAccount() == account)
2722                    && (conversation.getJid().asBareJid().equals(jid.asBareJid()))) {
2723                return conversation;
2724            }
2725        }
2726        return null;
2727    }
2728
2729    public boolean isConversationsListEmpty(final Conversation ignore) {
2730        synchronized (this.conversations) {
2731            final int size = this.conversations.size();
2732            return size == 0 || size == 1 && this.conversations.get(0) == ignore;
2733        }
2734    }
2735
2736    public boolean isConversationStillOpen(final Conversation conversation) {
2737        synchronized (this.conversations) {
2738            for (Conversation current : this.conversations) {
2739                if (current == conversation) {
2740                    return true;
2741                }
2742            }
2743        }
2744        return false;
2745    }
2746
2747    public void maybeRegisterWithMuc(Conversation c, String nickArg) {
2748        final var nick = nickArg == null ? c.getMucOptions().getSelf().getFullJid().getResource() : nickArg;
2749        final var register = new Iq(Iq.Type.GET);
2750        final var query0 = register.addChild("query");
2751        query0.setAttribute("xmlns", Namespace.REGISTER);
2752        register.setTo(c.getJid().asBareJid());
2753        sendIqPacket(c.getAccount(), register, (response) -> {
2754            if (response.getType() == Iq.Type.RESULT) {
2755                final Element query = response.addChild("query");
2756                query.setAttribute("xmlns", Namespace.REGISTER);
2757                String username = query.findChildContent("username", Namespace.REGISTER);
2758                if (username == null) username = query.findChildContent("nick", Namespace.REGISTER);
2759                if (username != null && username.equals(nick)) {
2760                    // Already registered with this nick, done
2761                    Log.d(Config.LOGTAG, "Already registered with " + c.getJid().asBareJid() + " as " + username);
2762                    return;
2763                }
2764                Data form = Data.parse(query.findChild("x", Namespace.DATA));
2765                if (form != null) {
2766                    final var field = form.getFieldByName("muc#register_roomnick");
2767                    if (field != null && nick.equals(field.getValue())) {
2768                        Log.d(Config.LOGTAG, "Already registered with " + c.getJid().asBareJid() + " as " + field.getValue());
2769                        return;
2770                    }
2771                }
2772                if (form == null || !"form".equals(form.getFormType()) || !form.getFields().stream().anyMatch(f -> f.isRequired() && !"muc#register_roomnick".equals(f.getFieldName()))) {
2773                    // No form, result form, or no required fields other than nickname, let's just send nickname
2774                    if (form == null || !"form".equals(form.getFormType())) {
2775                        form = new Data();
2776                        form.put("FORM_TYPE", "http://jabber.org/protocol/muc#register");
2777                    }
2778                    form.put("muc#register_roomnick", nick);
2779                    form.submit();
2780                    final var finish = new Iq(Iq.Type.SET);
2781                    final var query2 = finish.addChild("query");
2782                    query2.setAttribute("xmlns", Namespace.REGISTER);
2783                    query2.addChild(form);
2784                    finish.setTo(c.getJid().asBareJid());
2785                    sendIqPacket(c.getAccount(), finish, (response2) -> {
2786                        if (response.getType() == Iq.Type.RESULT) {
2787                            Log.w(Config.LOGTAG, "Success registering with channel " + c.getJid().asBareJid() + "/" + nick);
2788                        } else {
2789                            Log.w(Config.LOGTAG, "Error registering with channel: " + response2);
2790                        }
2791                    });
2792                } else {
2793                    // TODO: offer registration form to user
2794                    Log.d(Config.LOGTAG, "Complex registration form for " + c.getJid().asBareJid() + ": " + response);
2795                }
2796            } else {
2797                // We said maybe. Guess not
2798                Log.d(Config.LOGTAG, "Could not register with " + c.getJid().asBareJid() + ": " + response);
2799            }
2800        });
2801    }
2802
2803    public void deregisterWithMuc(Conversation c) {
2804        final Iq register = new Iq(Iq.Type.GET);
2805        final var query = register.addChild("query");
2806        query.setAttribute("xmlns", Namespace.REGISTER);
2807        query.addChild("remove");
2808        register.setTo(c.getJid().asBareJid());
2809        sendIqPacket(c.getAccount(), register, (response) -> {
2810            if (response.getType() == Iq.Type.RESULT) {
2811                Log.d(Config.LOGTAG, "deregistered with " + c.getJid().asBareJid());
2812            } else {
2813                Log.w(Config.LOGTAG, "Could not deregister with " + c.getJid().asBareJid() + ": " + response);
2814            }
2815        });
2816    }
2817
2818    public Conversation findOrCreateConversation(
2819            Account account, Jid jid, boolean muc, final boolean async) {
2820        return this.findOrCreateConversation(account, jid, muc, false, async);
2821    }
2822
2823    public Conversation findOrCreateConversation(
2824            final Account account,
2825            final Jid jid,
2826            final boolean muc,
2827            final boolean joinAfterCreate,
2828            final boolean async) {
2829        return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, null, async, null);
2830    }
2831
2832    public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final MessageArchiveService.Query query, final boolean async) {
2833        return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, query, async, null);
2834    }
2835
2836    public Conversation findOrCreateConversation(
2837            final Account account,
2838            final Jid jid,
2839            final boolean muc,
2840            final boolean joinAfterCreate,
2841            final MessageArchiveService.Query query,
2842            final boolean async,
2843            final String password) {
2844        synchronized (this.conversations) {
2845            final var cached = find(account, jid);
2846            if (cached != null) {
2847                return cached;
2848            }
2849            final var existing = databaseBackend.findConversation(account, jid);
2850            final Conversation conversation;
2851            final boolean loadMessagesFromDb;
2852            if (existing != null) {
2853                conversation = existing;
2854                if (password != null) conversation.getMucOptions().setPassword(password);
2855                loadMessagesFromDb = restoreFromArchive(conversation, jid, muc);
2856            } else {
2857                String conversationName;
2858                final Contact contact = account.getRoster().getContact(jid);
2859                if (contact != null) {
2860                    conversationName = contact.getDisplayName();
2861                } else {
2862                    conversationName = jid.getLocal();
2863                }
2864                if (muc) {
2865                    conversation =
2866                            new Conversation(
2867                                    conversationName, account, jid, Conversation.MODE_MULTI);
2868                } else {
2869                    conversation =
2870                            new Conversation(
2871                                    conversationName,
2872                                    account,
2873                                    jid.asBareJid(),
2874                                    Conversation.MODE_SINGLE);
2875                }
2876                if (password != null) conversation.getMucOptions().setPassword(password);
2877                this.databaseBackend.createConversation(conversation);
2878                loadMessagesFromDb = false;
2879            }
2880            if (async) {
2881                mDatabaseReaderExecutor.execute(
2882                        () ->
2883                                postProcessConversation(
2884                                        conversation, loadMessagesFromDb, joinAfterCreate, query));
2885            } else {
2886                postProcessConversation(conversation, loadMessagesFromDb, joinAfterCreate, query);
2887            }
2888            this.conversations.add(conversation);
2889            updateConversationUi();
2890            return conversation;
2891        }
2892    }
2893
2894    public Conversation findConversationByUuidReliable(final String uuid) {
2895        final var cached = findConversationByUuid(uuid);
2896        if (cached != null) {
2897            return cached;
2898        }
2899        final var existing = databaseBackend.findConversation(uuid);
2900        if (existing == null) {
2901            return null;
2902        }
2903        Log.d(Config.LOGTAG, "restoring conversation with " + existing.getJid() + " from DB");
2904        final Map<String, Account> accounts =
2905                ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
2906        final var account = accounts.get(existing.getAccountUuid());
2907        if (account == null) {
2908            Log.d(Config.LOGTAG, "could not find account " + existing.getAccountUuid());
2909            return null;
2910        }
2911        existing.setAccount(account);
2912        final var loadMessagesFromDb = restoreFromArchive(existing);
2913        mDatabaseReaderExecutor.execute(
2914                () ->
2915                        postProcessConversation(
2916                                existing,
2917                                loadMessagesFromDb,
2918                                existing.getMode() == Conversational.MODE_MULTI,
2919                                null));
2920        this.conversations.add(existing);
2921        if (existing.getMode() == Conversational.MODE_MULTI) {
2922            account.getXmppConnection()
2923                    .getManager(BookmarkManager.class)
2924                    .ensureBookmarkIsAutoJoin(existing);
2925        }
2926        updateConversationUi();
2927        return existing;
2928    }
2929
2930    private boolean restoreFromArchive(
2931            final Conversation conversation, final Jid jid, final boolean muc) {
2932        if (muc) {
2933            conversation.setMode(Conversation.MODE_MULTI);
2934            conversation.setContactJid(jid);
2935        } else {
2936            conversation.setMode(Conversation.MODE_SINGLE);
2937            conversation.setContactJid(jid.asBareJid());
2938        }
2939        return restoreFromArchive(conversation);
2940    }
2941
2942    private boolean restoreFromArchive(final Conversation conversation) {
2943        conversation.setStatus(Conversation.STATUS_AVAILABLE);
2944        databaseBackend.updateConversation(conversation);
2945        return conversation.messagesLoaded.compareAndSet(true, false);
2946    }
2947
2948    private void postProcessConversation(
2949            final Conversation c,
2950            final boolean loadMessagesFromDb,
2951            final boolean joinAfterCreate,
2952            final MessageArchiveService.Query query) {
2953        final var singleMode = c.getMode() == Conversational.MODE_SINGLE;
2954        final var account = c.getAccount();
2955        if (loadMessagesFromDb) {
2956            c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE), false);
2957            updateConversationUi();
2958            c.messagesLoaded.set(true);
2959        }
2960        if (account.getXmppConnection() != null
2961                && !c.getContact().isBlocked()
2962                && account.getXmppConnection().getFeatures().mam()
2963                && singleMode) {
2964            if (query == null) {
2965                mMessageArchiveService.query(c);
2966            } else {
2967                if (query.getConversation() == null) {
2968                    mMessageArchiveService.query(c, query.getStart(), query.isCatchup());
2969                }
2970            }
2971        }
2972        if (joinAfterCreate) {
2973            joinMuc(c);
2974        }
2975    }
2976
2977    public void archiveConversation(Conversation conversation) {
2978        archiveConversation(conversation, true);
2979    }
2980
2981    public void archiveConversation(
2982            Conversation conversation, final boolean maySynchronizeWithBookmarks) {
2983        if (isOnboarding()) return;
2984
2985        final var account = conversation.getAccount();
2986        final var connection = account.getXmppConnection();
2987        getNotificationService().clear(conversation);
2988        conversation.setStatus(Conversation.STATUS_ARCHIVED);
2989        conversation.setNextMessage(null);
2990        synchronized (this.conversations) {
2991            getMessageArchiveService().kill(conversation);
2992            if (conversation.getMode() == Conversation.MODE_MULTI) {
2993                // TODO always clean up bookmarks no matter if we are currently connected
2994                // TODO always delete reference to conversation in bookmark
2995                if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
2996                    final Bookmark bookmark = conversation.getBookmark();
2997                    if (maySynchronizeWithBookmarks && bookmark != null) {
2998                        if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) {
2999                            bookmark.setConversation(null);
3000                            deleteBookmark(account, bookmark);
3001                        } else if (bookmark.autojoin()) {
3002                            bookmark.setAutojoin(false);
3003                            createBookmark(bookmark.getAccount(), bookmark);
3004                        }
3005                    }
3006                }
3007                deregisterWithMuc(conversation);
3008                connection.getManager(MultiUserChatManager.class).leave(conversation);
3009            } else {
3010                if (conversation
3011                        .getContact()
3012                        .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
3013                    stopPresenceUpdatesTo(conversation.getContact());
3014                }
3015            }
3016            updateConversation(conversation);
3017            this.conversations.remove(conversation);
3018            updateConversationUi();
3019        }
3020    }
3021
3022    public void stopPresenceUpdatesTo(final Contact contact) {
3023        Log.d(Config.LOGTAG, "Canceling presence request from " + contact.getJid().toString());
3024        contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
3025        contact.getAccount()
3026                .getXmppConnection()
3027                .getManager(PresenceManager.class)
3028                .unsubscribed(contact.getJid().asBareJid());
3029    }
3030
3031    public void createAccount(final Account account) {
3032        account.setXmppConnection(createConnection(account));
3033        databaseBackend.createAccount(account);
3034        if (CallIntegration.hasSystemFeature(this)) {
3035            CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
3036        }
3037        this.accounts.add(account);
3038        this.reconnectAccountInBackground(account);
3039        updateAccountUi();
3040        syncEnabledAccountSetting();
3041        toggleForegroundService();
3042    }
3043
3044    private void syncEnabledAccountSetting() {
3045        final boolean hasEnabledAccounts = hasEnabledAccounts();
3046        getPreferences()
3047                .edit()
3048                .putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts)
3049                .apply();
3050        toggleSetProfilePictureActivity(hasEnabledAccounts);
3051    }
3052
3053    private void toggleSetProfilePictureActivity(final boolean enabled) {
3054        try {
3055            final ComponentName name =
3056                    new ComponentName(this, ChooseAccountForProfilePictureActivity.class);
3057            final int targetState =
3058                    enabled
3059                            ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
3060                            : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
3061            getPackageManager()
3062                    .setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP);
3063        } catch (IllegalStateException e) {
3064            Log.d(Config.LOGTAG, "unable to toggle profile picture activity");
3065        }
3066    }
3067
3068    public boolean reconfigurePushDistributor() {
3069        return this.unifiedPushBroker.reconfigurePushDistributor();
3070    }
3071
3072    private Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints(
3073            final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger) {
3074        return this.unifiedPushBroker.renewUnifiedPushEndpoints(pushTargetMessenger);
3075    }
3076
3077    public Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints() {
3078        return this.unifiedPushBroker.renewUnifiedPushEndpoints(null);
3079    }
3080
3081    public UnifiedPushBroker getUnifiedPushBroker() {
3082        return this.unifiedPushBroker;
3083    }
3084
3085    private void provisionAccount(final String address, final String password) {
3086        final Jid jid = Jid.of(address);
3087        final Account account = new Account(jid, password);
3088        account.setOption(Account.OPTION_DISABLED, true);
3089        Log.d(Config.LOGTAG, jid.asBareJid().toString() + ": provisioning account");
3090        createAccount(account);
3091    }
3092
3093    public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
3094        new Thread(
3095                        () -> {
3096                            try {
3097                                final X509Certificate[] chain =
3098                                        KeyChain.getCertificateChain(this, alias);
3099                                final X509Certificate cert =
3100                                        chain != null && chain.length > 0 ? chain[0] : null;
3101                                if (cert == null) {
3102                                    callback.informUser(R.string.unable_to_parse_certificate);
3103                                    return;
3104                                }
3105                                Pair<Jid, String> info = CryptoHelper.extractJidAndName(cert);
3106                                if (info == null) {
3107                                    callback.informUser(R.string.certificate_does_not_contain_jid);
3108                                    return;
3109                                }
3110                                if (findAccountByJid(info.first) == null) {
3111                                    final Account account = new Account(info.first, "");
3112                                    account.setPrivateKeyAlias(alias);
3113                                    account.setOption(Account.OPTION_DISABLED, true);
3114                                    account.setOption(Account.OPTION_FIXED_USERNAME, true);
3115                                    account.setDisplayName(info.second);
3116                                    createAccount(account);
3117                                    callback.onAccountCreated(account);
3118                                    if (Config.X509_VERIFICATION) {
3119                                        try {
3120                                            getMemorizingTrustManager()
3121                                                    .getNonInteractive(account.getServer(), null, 0, null)
3122                                                    .checkClientTrusted(chain, "RSA");
3123                                        } catch (CertificateException e) {
3124                                            callback.informUser(
3125                                                    R.string.certificate_chain_is_not_trusted);
3126                                        }
3127                                    }
3128                                } else {
3129                                    callback.informUser(R.string.account_already_exists);
3130                                }
3131                            } catch (Exception e) {
3132                                callback.informUser(R.string.unable_to_parse_certificate);
3133                            }
3134                        })
3135                .start();
3136    }
3137
3138    public void updateKeyInAccount(final Account account, final String alias) {
3139        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": update key in account " + alias);
3140        try {
3141            X509Certificate[] chain =
3142                    KeyChain.getCertificateChain(XmppConnectionService.this, alias);
3143            Log.d(Config.LOGTAG, account.getJid().asBareJid() + " loaded certificate chain");
3144            Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
3145            if (info == null) {
3146                showErrorToastInUi(R.string.certificate_does_not_contain_jid);
3147                return;
3148            }
3149            if (account.getJid().asBareJid().equals(info.first)) {
3150                account.setPrivateKeyAlias(alias);
3151                account.setDisplayName(info.second);
3152                databaseBackend.updateAccount(account);
3153                if (Config.X509_VERIFICATION) {
3154                    try {
3155                        getMemorizingTrustManager()
3156                                .getNonInteractive()
3157                                .checkClientTrusted(chain, "RSA");
3158                    } catch (CertificateException e) {
3159                        showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
3160                    }
3161                    account.getAxolotlService().regenerateKeys(true);
3162                }
3163            } else {
3164                showErrorToastInUi(R.string.jid_does_not_match_certificate);
3165            }
3166        } catch (Exception e) {
3167            e.printStackTrace();
3168        }
3169    }
3170
3171    public boolean updateAccount(final Account account) {
3172        if (databaseBackend.updateAccount(account)) {
3173            Integer color = account.getColorToSave();
3174            if (color == null) {
3175                getPreferences().edit().remove("account_color:" + account.getUuid()).commit();
3176            } else {
3177                getPreferences().edit().putInt("account_color:" + account.getUuid(), color.intValue()).commit();
3178            }
3179            account.setShowErrorNotification(true);
3180            // TODO what was the purpose of that? will likely be triggered by reconnect anyway?
3181            // this.statusListener.onStatusChanged(account);
3182            databaseBackend.updateAccount(account);
3183            reconnectAccountInBackground(account);
3184            updateAccountUi();
3185            getNotificationService().updateErrorNotification();
3186            toggleForegroundService();
3187            syncEnabledAccountSetting();
3188            mChannelDiscoveryService.cleanCache();
3189            if (CallIntegration.hasSystemFeature(this)) {
3190                CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
3191            }
3192            return true;
3193        } else {
3194            return false;
3195        }
3196    }
3197
3198    public ListenableFuture<Void> updateAccountPasswordOnServer(
3199            final Account account, final String newPassword) {
3200        final var connection = account.getXmppConnection();
3201        return connection.getManager(RegistrationManager.class).setPassword(newPassword);
3202    }
3203
3204    public void deleteAccount(final Account account) {
3205        getPreferences().edit().remove("onboarding_continued").commit();
3206        final boolean connected = account.getStatus() == Account.State.ONLINE;
3207        synchronized (this.conversations) {
3208            if (connected) {
3209                account.getAxolotlService().deleteOmemoIdentity();
3210            }
3211            for (final Conversation conversation : conversations) {
3212                if (conversation.getAccount() == account) {
3213                    if (conversation.getMode() == Conversation.MODE_MULTI) {
3214                        if (connected) {
3215                            account.getXmppConnection()
3216                                    .getManager(MultiUserChatManager.class)
3217                                    .unavailable(conversation);
3218                        }
3219                    }
3220                    conversations.remove(conversation);
3221                    mNotificationService.clear(conversation);
3222                }
3223            }
3224            new Thread(() -> {
3225                for (final Contact contact : account.getRoster().getContacts()) {
3226                    contact.unregisterAsPhoneAccount(this);
3227                }
3228            }).start();
3229            if (account.getXmppConnection() != null) {
3230                new Thread(() -> disconnect(account, !connected)).start();
3231            }
3232            final Runnable runnable =
3233                    () -> {
3234                        if (!databaseBackend.deleteAccount(account)) {
3235                            Log.d(
3236                                    Config.LOGTAG,
3237                                    account.getJid().asBareJid() + ": unable to delete account");
3238                        }
3239                    };
3240            mDatabaseWriterExecutor.execute(runnable);
3241            this.accounts.remove(account);
3242            if (CallIntegration.hasSystemFeature(this)) {
3243                CallIntegrationConnectionService.unregisterPhoneAccount(this, account);
3244            }
3245            updateAccountUi();
3246            mNotificationService.updateErrorNotification();
3247            syncEnabledAccountSetting();
3248            toggleForegroundService();
3249        }
3250    }
3251
3252    public void setOnConversationListChangedListener(OnConversationUpdate listener) {
3253        final boolean remainingListeners;
3254        synchronized (LISTENER_LOCK) {
3255            remainingListeners = checkListeners();
3256            if (!this.mOnConversationUpdates.add(listener)) {
3257                Log.w(
3258                        Config.LOGTAG,
3259                        listener.getClass().getName()
3260                                + " is already registered as ConversationListChangedListener");
3261            }
3262            this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
3263        }
3264        if (remainingListeners) {
3265            switchToForeground();
3266        }
3267    }
3268
3269    public void removeOnConversationListChangedListener(OnConversationUpdate listener) {
3270        final boolean remainingListeners;
3271        synchronized (LISTENER_LOCK) {
3272            this.mOnConversationUpdates.remove(listener);
3273            this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
3274            remainingListeners = checkListeners();
3275        }
3276        if (remainingListeners) {
3277            switchToBackground();
3278        }
3279    }
3280
3281    public void setOnShowErrorToastListener(OnShowErrorToast listener) {
3282        final boolean remainingListeners;
3283        synchronized (LISTENER_LOCK) {
3284            remainingListeners = checkListeners();
3285            if (!this.mOnShowErrorToasts.add(listener)) {
3286                Log.w(
3287                        Config.LOGTAG,
3288                        listener.getClass().getName()
3289                                + " is already registered as OnShowErrorToastListener");
3290            }
3291        }
3292        if (remainingListeners) {
3293            switchToForeground();
3294        }
3295    }
3296
3297    public void removeOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
3298        final boolean remainingListeners;
3299        synchronized (LISTENER_LOCK) {
3300            this.mOnShowErrorToasts.remove(onShowErrorToast);
3301            remainingListeners = checkListeners();
3302        }
3303        if (remainingListeners) {
3304            switchToBackground();
3305        }
3306    }
3307
3308    public void setOnAccountListChangedListener(OnAccountUpdate listener) {
3309        final boolean remainingListeners;
3310        synchronized (LISTENER_LOCK) {
3311            remainingListeners = checkListeners();
3312            if (!this.mOnAccountUpdates.add(listener)) {
3313                Log.w(
3314                        Config.LOGTAG,
3315                        listener.getClass().getName()
3316                                + " is already registered as OnAccountListChangedtListener");
3317            }
3318        }
3319        if (remainingListeners) {
3320            switchToForeground();
3321        }
3322    }
3323
3324    public void removeOnAccountListChangedListener(OnAccountUpdate listener) {
3325        final boolean remainingListeners;
3326        synchronized (LISTENER_LOCK) {
3327            this.mOnAccountUpdates.remove(listener);
3328            remainingListeners = checkListeners();
3329        }
3330        if (remainingListeners) {
3331            switchToBackground();
3332        }
3333    }
3334
3335    public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
3336        final boolean remainingListeners;
3337        synchronized (LISTENER_LOCK) {
3338            remainingListeners = checkListeners();
3339            if (!this.mOnCaptchaRequested.add(listener)) {
3340                Log.w(
3341                        Config.LOGTAG,
3342                        listener.getClass().getName()
3343                                + " is already registered as OnCaptchaRequestListener");
3344            }
3345        }
3346        if (remainingListeners) {
3347            switchToForeground();
3348        }
3349    }
3350
3351    public void removeOnCaptchaRequestedListener(OnCaptchaRequested listener) {
3352        final boolean remainingListeners;
3353        synchronized (LISTENER_LOCK) {
3354            this.mOnCaptchaRequested.remove(listener);
3355            remainingListeners = checkListeners();
3356        }
3357        if (remainingListeners) {
3358            switchToBackground();
3359        }
3360    }
3361
3362    public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
3363        final boolean remainingListeners;
3364        synchronized (LISTENER_LOCK) {
3365            remainingListeners = checkListeners();
3366            if (!this.mOnRosterUpdates.add(listener)) {
3367                Log.w(
3368                        Config.LOGTAG,
3369                        listener.getClass().getName()
3370                                + " is already registered as OnRosterUpdateListener");
3371            }
3372        }
3373        if (remainingListeners) {
3374            switchToForeground();
3375        }
3376    }
3377
3378    public void removeOnRosterUpdateListener(final OnRosterUpdate listener) {
3379        final boolean remainingListeners;
3380        synchronized (LISTENER_LOCK) {
3381            this.mOnRosterUpdates.remove(listener);
3382            remainingListeners = checkListeners();
3383        }
3384        if (remainingListeners) {
3385            switchToBackground();
3386        }
3387    }
3388
3389    public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
3390        final boolean remainingListeners;
3391        synchronized (LISTENER_LOCK) {
3392            remainingListeners = checkListeners();
3393            if (!this.mOnUpdateBlocklist.add(listener)) {
3394                Log.w(
3395                        Config.LOGTAG,
3396                        listener.getClass().getName()
3397                                + " is already registered as OnUpdateBlocklistListener");
3398            }
3399        }
3400        if (remainingListeners) {
3401            switchToForeground();
3402        }
3403    }
3404
3405    public void removeOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
3406        final boolean remainingListeners;
3407        synchronized (LISTENER_LOCK) {
3408            this.mOnUpdateBlocklist.remove(listener);
3409            remainingListeners = checkListeners();
3410        }
3411        if (remainingListeners) {
3412            switchToBackground();
3413        }
3414    }
3415
3416    public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
3417        final boolean remainingListeners;
3418        synchronized (LISTENER_LOCK) {
3419            remainingListeners = checkListeners();
3420            if (!this.mOnKeyStatusUpdated.add(listener)) {
3421                Log.w(
3422                        Config.LOGTAG,
3423                        listener.getClass().getName()
3424                                + " is already registered as OnKeyStatusUpdateListener");
3425            }
3426        }
3427        if (remainingListeners) {
3428            switchToForeground();
3429        }
3430    }
3431
3432    public void removeOnNewKeysAvailableListener(final OnKeyStatusUpdated listener) {
3433        final boolean remainingListeners;
3434        synchronized (LISTENER_LOCK) {
3435            this.mOnKeyStatusUpdated.remove(listener);
3436            remainingListeners = checkListeners();
3437        }
3438        if (remainingListeners) {
3439            switchToBackground();
3440        }
3441    }
3442
3443    public void setOnRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
3444        final boolean remainingListeners;
3445        synchronized (LISTENER_LOCK) {
3446            remainingListeners = checkListeners();
3447            if (!this.onJingleRtpConnectionUpdate.add(listener)) {
3448                Log.w(
3449                        Config.LOGTAG,
3450                        listener.getClass().getName()
3451                                + " is already registered as OnJingleRtpConnectionUpdate");
3452            }
3453        }
3454        if (remainingListeners) {
3455            switchToForeground();
3456        }
3457    }
3458
3459    public void removeRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
3460        final boolean remainingListeners;
3461        synchronized (LISTENER_LOCK) {
3462            this.onJingleRtpConnectionUpdate.remove(listener);
3463            remainingListeners = checkListeners();
3464        }
3465        if (remainingListeners) {
3466            switchToBackground();
3467        }
3468    }
3469
3470    public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
3471        final boolean remainingListeners;
3472        synchronized (LISTENER_LOCK) {
3473            remainingListeners = checkListeners();
3474            if (!this.mOnMucRosterUpdate.add(listener)) {
3475                Log.w(
3476                        Config.LOGTAG,
3477                        listener.getClass().getName()
3478                                + " is already registered as OnMucRosterListener");
3479            }
3480        }
3481        if (remainingListeners) {
3482            switchToForeground();
3483        }
3484    }
3485
3486    public void removeOnMucRosterUpdateListener(final OnMucRosterUpdate listener) {
3487        final boolean remainingListeners;
3488        synchronized (LISTENER_LOCK) {
3489            this.mOnMucRosterUpdate.remove(listener);
3490            remainingListeners = checkListeners();
3491        }
3492        if (remainingListeners) {
3493            switchToBackground();
3494        }
3495    }
3496
3497    public boolean checkListeners() {
3498        return (this.mOnAccountUpdates.isEmpty()
3499                && this.mOnConversationUpdates.isEmpty()
3500                && this.mOnRosterUpdates.isEmpty()
3501                && this.mOnCaptchaRequested.isEmpty()
3502                && this.mOnMucRosterUpdate.isEmpty()
3503                && this.mOnUpdateBlocklist.isEmpty()
3504                && this.mOnShowErrorToasts.isEmpty()
3505                && this.onJingleRtpConnectionUpdate.isEmpty()
3506                && this.mOnKeyStatusUpdated.isEmpty());
3507    }
3508
3509    private void switchToForeground() {
3510        toggleSoftDisabled(false);
3511        final boolean broadcastLastActivity = appSettings.isBroadcastLastActivity();
3512        for (Conversation conversation : getConversations()) {
3513            if (conversation.getMode() == Conversation.MODE_MULTI) {
3514                conversation.getMucOptions().resetChatState();
3515            } else {
3516                conversation.setIncomingChatState(Config.DEFAULT_CHAT_STATE);
3517            }
3518        }
3519        for (final var account : getAccounts()) {
3520            if (account.getStatus() != Account.State.ONLINE) {
3521                continue;
3522            }
3523            account.deactivateGracePeriod();
3524            final XmppConnection connection = account.getXmppConnection();
3525            if (connection.getFeatures().csi()) {
3526                connection.sendActive();
3527            }
3528            if (broadcastLastActivity) {
3529                // send new presence but don't include idle because we are not
3530                connection.getManager(PresenceManager.class).available(false);
3531            }
3532        }
3533        Log.d(Config.LOGTAG, "app switched into foreground");
3534    }
3535
3536    private void switchToBackground() {
3537        final boolean broadcastLastActivity = appSettings.isBroadcastLastActivity();
3538        if (broadcastLastActivity) {
3539            mLastActivity = System.currentTimeMillis();
3540            final SharedPreferences.Editor editor = getPreferences().edit();
3541            editor.putLong(SETTING_LAST_ACTIVITY_TS, mLastActivity);
3542            editor.apply();
3543        }
3544        for (final var account : getAccounts()) {
3545            if (account.getStatus() != Account.State.ONLINE) {
3546                continue;
3547            }
3548            final var connection = account.getXmppConnection();
3549            if (broadcastLastActivity) {
3550                connection.getManager(PresenceManager.class).available(true);
3551            }
3552            if (connection.getFeatures().csi()) {
3553                connection.sendInactive();
3554            }
3555        }
3556        this.mNotificationService.setIsInForeground(false);
3557        Log.d(Config.LOGTAG, "app switched into background");
3558    }
3559
3560    public void connectMultiModeConversations(Account account) {
3561        List<Conversation> conversations = getConversations();
3562        for (Conversation conversation : conversations) {
3563            if (conversation.getMode() == Conversation.MODE_MULTI
3564                    && conversation.getAccount() == account) {
3565                joinMuc(conversation);
3566            }
3567        }
3568    }
3569
3570    public void joinMuc(final Conversation conversation) {
3571        final var account = conversation.getAccount();
3572        account.getXmppConnection().getManager(MultiUserChatManager.class).join(conversation);
3573    }
3574
3575    public void providePasswordForMuc(final Conversation conversation, final String password) {
3576        final var account = conversation.getAccount();
3577        account.getXmppConnection()
3578                .getManager(MultiUserChatManager.class)
3579                .setPassword(conversation, password);
3580    }
3581
3582    public void deleteAvatar(final Account account) {
3583        final var connection = account.getXmppConnection();
3584
3585        final var vCardPhotoDeletionFuture =
3586                connection.getManager(VCardManager.class).deletePhoto();
3587        final var pepDeletionFuture = connection.getManager(AvatarManager.class).delete();
3588
3589        final var deletionFuture = Futures.allAsList(vCardPhotoDeletionFuture, pepDeletionFuture);
3590
3591        Futures.addCallback(
3592                deletionFuture,
3593                new FutureCallback<>() {
3594                    @Override
3595                    public void onSuccess(List<Void> result) {
3596                        Log.d(
3597                                Config.LOGTAG,
3598                                account.getJid().asBareJid() + ": deleted avatar from server");
3599                        account.setAvatar(null);
3600                        databaseBackend.updateAccount(account);
3601                        getAvatarService().clear(account);
3602                        updateAccountUi();
3603                    }
3604
3605                    @Override
3606                    public void onFailure(Throwable t) {
3607                        Log.d(
3608                                Config.LOGTAG,
3609                                account.getJid().asBareJid() + ": could not delete avatar",
3610                                t);
3611                    }
3612                },
3613                MoreExecutors.directExecutor());
3614    }
3615
3616    public void deletePepNode(final Account account, final String node) {
3617        final var future = account.getXmppConnection().getManager(PepManager.class).delete(node);
3618        Futures.addCallback(
3619                future,
3620                new FutureCallback<Void>() {
3621                    @Override
3622                    public void onSuccess(Void result) {
3623                        Log.d(
3624                                Config.LOGTAG,
3625                                account.getJid().asBareJid()
3626                                        + ": successfully deleted pep node "
3627                                        + node);
3628                    }
3629
3630                    @Override
3631                    public void onFailure(@NonNull Throwable t) {
3632                        Log.d(
3633                                Config.LOGTAG,
3634                                account.getJid().asBareJid() + ": failed to delete node " + node,
3635                                t);
3636                    }
3637                },
3638                MoreExecutors.directExecutor());
3639    }
3640
3641    private boolean hasEnabledAccounts() {
3642        if (this.accounts == null) {
3643            return false;
3644        }
3645        for (final Account account : this.accounts) {
3646            if (account.isConnectionEnabled()) {
3647                return true;
3648            }
3649        }
3650        return false;
3651    }
3652
3653    public void getAttachments(
3654            final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) {
3655        getAttachments(
3656                conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded);
3657    }
3658
3659    public void getAttachments(
3660            final Account account,
3661            final Jid jid,
3662            final int limit,
3663            final OnMediaLoaded onMediaLoaded) {
3664        getAttachments(account.getUuid(), jid.asBareJid(), limit, onMediaLoaded);
3665    }
3666
3667    public void getAttachments(
3668            final String account,
3669            final Jid jid,
3670            final int limit,
3671            final OnMediaLoaded onMediaLoaded) {
3672        new Thread(
3673                        () ->
3674                                onMediaLoaded.onMediaLoaded(
3675                                        fileBackend.convertToAttachments(
3676                                                databaseBackend.getRelativeFilePaths(
3677                                                        account, jid, limit))))
3678                .start();
3679    }
3680
3681    public void persistSelfNick(final MucOptions.User self, final boolean modified) {
3682        final Conversation conversation = self.getConversation();
3683        final Account account = conversation.getAccount();
3684        final Jid full = self.getFullJid();
3685        if (!full.equals(conversation.getJid())) {
3686            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persisting full jid " + full);
3687            conversation.setContactJid(full);
3688            databaseBackend.updateConversation(conversation);
3689        }
3690
3691        final String nick = self.getNick();
3692        final Bookmark bookmark = conversation.getBookmark();
3693        if (bookmark == null || !modified) {
3694            return;
3695        }
3696        final String defaultNick = MucOptions.defaultNick(account);
3697        if (nick.equals(defaultNick) || nick.equals(bookmark.getNick())) {
3698            return;
3699        }
3700        Log.d(
3701                Config.LOGTAG,
3702                account.getJid().asBareJid()
3703                        + ": persist nick '"
3704                        + full.getResource()
3705                        + "' into bookmark for "
3706                        + conversation.getJid().asBareJid());
3707        bookmark.setNick(nick);
3708        createBookmark(bookmark.getAccount(), bookmark);
3709    }
3710
3711    public void checkMucRequiresRename() {
3712        synchronized (this.conversations) {
3713            for (final Conversation conversation : this.conversations) {
3714                if (conversation.getMode() == Conversational.MODE_MULTI) {
3715                    final var account = conversation.getAccount();
3716                    account.getXmppConnection()
3717                            .getManager(MultiUserChatManager.class)
3718                            .checkMucRequiresRename(conversation);
3719                }
3720            }
3721        }
3722    }
3723
3724    public void createPublicChannel(
3725            final Account account,
3726            final String name,
3727            final Jid address,
3728            final UiCallback<Conversation> callback) {
3729        final var future =
3730                account.getXmppConnection()
3731                        .getManager(MultiUserChatManager.class)
3732                        .createPublicChannel(address, name);
3733
3734        Futures.addCallback(
3735                future,
3736                new FutureCallback<Conversation>() {
3737                    @Override
3738                    public void onSuccess(Conversation result) {
3739                        callback.success(result);
3740                    }
3741
3742                    @Override
3743                    public void onFailure(Throwable t) {
3744                        Log.d(Config.LOGTAG, "could not create public channel", t);
3745                        // TODO I guess it’s better to just not use callbacks here
3746                        callback.error(R.string.unable_to_set_channel_configuration, null);
3747                    }
3748                },
3749                MoreExecutors.directExecutor());
3750    }
3751
3752    public void checkIfMuc(final Account account, final Jid jid, Consumer<Boolean> cb) {
3753        if (jid.isDomainJid()) {
3754            // Spec basically says MUC needs to have a node
3755            // And also specifies that MUC and MUC service should have the same identity...
3756            cb.accept(false);
3757            return;
3758        }
3759
3760        final var connection = account.getXmppConnection();
3761        if (connection == null) {
3762            cb.accept(false); // hmmm...
3763            return;
3764        }
3765        final ListenableFuture<InfoQuery> future =
3766                connection
3767                        .getManager(DiscoManager.class)
3768                        .info(Entity.discoItem(jid), null);
3769
3770        Futures.addCallback(
3771                future,
3772                new FutureCallback<>() {
3773                    @Override
3774                    public void onSuccess(InfoQuery result) {
3775                        cb.accept(
3776                            result.hasFeature("http://jabber.org/protocol/muc") &&
3777                            result.hasIdentityWithCategory("conference")
3778                        );
3779                    }
3780
3781                    @Override
3782                    public void onFailure(@NonNull Throwable throwable) {
3783                        cb.accept(false);
3784                    }
3785                },
3786                MoreExecutors.directExecutor()
3787        );
3788    }
3789
3790    public boolean createAdhocConference(
3791            final Account account,
3792            final String name,
3793            final Collection<Jid> addresses,
3794            final UiCallback<Conversation> callback) {
3795        final var manager = account.getXmppConnection().getManager(MultiUserChatManager.class);
3796        if (manager.getServices().isEmpty()) {
3797            return false;
3798        }
3799
3800        final var future = manager.createPrivateGroupChat(name, addresses);
3801
3802        Futures.addCallback(
3803                future,
3804                new FutureCallback<>() {
3805                    @Override
3806                    public void onSuccess(Conversation result) {
3807                        callback.success(result);
3808                    }
3809
3810                    @Override
3811                    public void onFailure(@NonNull Throwable t) {
3812                        Log.d(Config.LOGTAG, "could not create private group chat", t);
3813                        callback.error(R.string.conference_creation_failed, null);
3814                    }
3815                },
3816                MoreExecutors.directExecutor());
3817        return true;
3818    }
3819
3820    public void pushNodeConfiguration(
3821            Account account,
3822            final String node,
3823            final Bundle options,
3824            final OnConfigurationPushed callback) {
3825        pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback);
3826    }
3827
3828    public void pushNodeConfiguration(
3829            Account account,
3830            final Jid jid,
3831            final String node,
3832            final Bundle options,
3833            final OnConfigurationPushed callback) {
3834        Log.d(Config.LOGTAG, "pushing node configuration");
3835        sendIqPacket(
3836                account,
3837                mIqGenerator.requestPubsubConfiguration(jid, node),
3838                responseToRequest -> {
3839                    if (responseToRequest.getType() == Iq.Type.RESULT) {
3840                        Element pubsub =
3841                                responseToRequest.findChild(
3842                                        "pubsub", "http://jabber.org/protocol/pubsub#owner");
3843                        Element configuration =
3844                                pubsub == null ? null : pubsub.findChild("configure");
3845                        Element x =
3846                                configuration == null
3847                                        ? null
3848                                        : configuration.findChild("x", Namespace.DATA);
3849                        if (x != null) {
3850                            final Data data = Data.parse(x);
3851                            data.submit(options);
3852                            sendIqPacket(
3853                                    account,
3854                                    mIqGenerator.publishPubsubConfiguration(jid, node, data),
3855                                    responseToPublish -> {
3856                                        if (responseToPublish.getType() == Iq.Type.RESULT
3857                                                && callback != null) {
3858                                            Log.d(
3859                                                    Config.LOGTAG,
3860                                                    account.getJid().asBareJid()
3861                                                            + ": successfully changed node"
3862                                                            + " configuration for node "
3863                                                            + node);
3864                                            callback.onPushSucceeded();
3865                                        } else if (responseToPublish.getType() == Iq.Type.ERROR
3866                                                && callback != null) {
3867                                            callback.onPushFailed();
3868                                        }
3869                                    });
3870                        } else if (callback != null) {
3871                            callback.onPushFailed();
3872                        }
3873                    } else if (responseToRequest.getType() == Iq.Type.ERROR && callback != null) {
3874                        callback.onPushFailed();
3875                    }
3876                });
3877    }
3878
3879    public void pushSubjectToConference(final Conversation conference, final String subject) {
3880        final var account = conference.getAccount();
3881        account.getXmppConnection()
3882                .getManager(MultiUserChatManager.class)
3883                .setSubject(conference, subject);
3884    }
3885
3886    public void requestVoice(final Account account, final Jid jid) {
3887        final var packet = this.getMessageGenerator().requestVoice(jid);
3888        this.sendMessagePacket(account, packet);
3889    }
3890
3891    public void changeAffiliationInConference(
3892            final Conversation conference,
3893            Jid user,
3894            final Affiliation affiliation,
3895            final OnAffiliationChanged callback) {
3896        final var account = conference.getAccount();
3897        final var future =
3898                account.getXmppConnection()
3899                        .getManager(MultiUserChatManager.class)
3900                        .setAffiliation(conference, affiliation, user);
3901        Futures.addCallback(
3902                future,
3903                new FutureCallback<Void>() {
3904                    @Override
3905                    public void onSuccess(Void result) {
3906                        if (callback != null) {
3907                            callback.onAffiliationChangedSuccessful(user);
3908                        } else {
3909                            Log.d(
3910                                    Config.LOGTAG,
3911                                    "changed affiliation of " + user + " to " + affiliation);
3912                        }
3913                    }
3914
3915                    @Override
3916                    public void onFailure(Throwable t) {
3917                        if (callback != null) {
3918                            callback.onAffiliationChangeFailed(
3919                                    user, R.string.could_not_change_affiliation);
3920                        } else {
3921                            Log.d(Config.LOGTAG, "could not change affiliation", t);
3922                        }
3923                    }
3924                },
3925                MoreExecutors.directExecutor());
3926    }
3927
3928    public void changeRoleInConference(
3929            final Conversation conference, final String nick, Role role) {
3930        final var account = conference.getAccount();
3931        account.getXmppConnection()
3932                .getManager(MultiUserChatManager.class)
3933                .setRole(conference.getJid().asBareJid(), role, nick);
3934    }
3935
3936    public void moderateMessage(final Account account, final Message m, final String reason) {
3937        final var request = this.mIqGenerator.moderateMessage(account, m, reason);
3938        sendIqPacket(account, request, (packet) -> {
3939            if (packet.getType() != Iq.Type.RESULT) {
3940                showErrorToastInUi(R.string.unable_to_moderate);
3941                Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to moderate: " + packet);
3942            }
3943        });
3944    }
3945
3946    public ListenableFuture<Void> destroyRoom(final Conversation conversation) {
3947        final var account = conversation.getAccount();
3948        return account.getXmppConnection()
3949                .getManager(MultiUserChatManager.class)
3950                .destroy(conversation.getJid().asBareJid());
3951    }
3952
3953    private void disconnect(final Account account, boolean force) {
3954        final XmppConnection connection = account.getXmppConnection();
3955        if (connection == null) {
3956            return;
3957        }
3958        if (!force) {
3959            final List<Conversation> conversations = getConversations();
3960            for (Conversation conversation : conversations) {
3961                if (conversation.getAccount() == account) {
3962                    if (conversation.getMode() == Conversation.MODE_MULTI) {
3963                        account.getXmppConnection()
3964                                .getManager(MultiUserChatManager.class)
3965                                .unavailable(conversation);
3966                    }
3967                }
3968            }
3969            connection.getManager(PresenceManager.class).unavailable();
3970        }
3971        connection.disconnect(force);
3972    }
3973
3974    @Override
3975    public IBinder onBind(Intent intent) {
3976        return mBinder;
3977    }
3978
3979    public void deleteMessage(Message message) {
3980        mScheduledMessages.remove(message.getUuid());
3981        databaseBackend.deleteMessage(message.getUuid());
3982        ((Conversation) message.getConversation()).remove(message);
3983        updateConversationUi();
3984    }
3985
3986    public void updateMessage(Message message) {
3987        updateMessage(message, true);
3988    }
3989
3990    public void updateMessage(Message message, boolean includeBody) {
3991        databaseBackend.updateMessage(message, includeBody);
3992        updateConversationUi();
3993    }
3994
3995    public void createMessageAsync(final Message message) {
3996        mDatabaseWriterExecutor.execute(() -> databaseBackend.createMessage(message));
3997    }
3998
3999    public void updateMessage(Message message, String uuid) {
4000        if (!databaseBackend.updateMessage(message, uuid)) {
4001            Log.e(Config.LOGTAG, "error updated message in DB after edit");
4002        }
4003        updateConversationUi();
4004    }
4005
4006    public void createContact(final Contact contact) {
4007        createContact(contact, null);
4008    }
4009
4010    public void unregisterPhoneAccounts(final Account account) {
4011        for (final Contact contact : account.getRoster().getContacts()) {
4012            if (!contact.showInRoster()) {
4013                contact.unregisterAsPhoneAccount(this);
4014            }
4015        }
4016    }
4017
4018    public void createContact(final Contact contact, final String preAuth) {
4019        contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
4020        contact.setOption(Contact.Options.ASKING);
4021        final var connection = contact.getAccount().getXmppConnection();
4022        connection.getManager(RosterManager.class).addRosterItem(contact, preAuth);
4023    }
4024
4025    public void deleteContactOnServer(final Contact contact) {
4026        final var connection = contact.getAccount().getXmppConnection();
4027        connection.getManager(RosterManager.class).deleteRosterItem(contact);
4028    }
4029
4030    public void publishMucAvatar(
4031            final Conversation conversation, final Uri image, final OnAvatarPublication callback) {
4032        final var connection = conversation.getAccount().getXmppConnection();
4033        final var future =
4034                connection
4035                        .getManager(AvatarManager.class)
4036                        .publishVCard(conversation.getJid().asBareJid(), image);
4037        Futures.addCallback(
4038                future,
4039                new FutureCallback<>() {
4040                    @Override
4041                    public void onSuccess(Void result) {
4042                        callback.onAvatarPublicationSucceeded();
4043                    }
4044
4045                    @Override
4046                    public void onFailure(@NonNull Throwable t) {
4047                        Log.d(Config.LOGTAG, "could not publish MUC avatar", t);
4048                        callback.onAvatarPublicationFailed(
4049                                R.string.error_publish_avatar_server_reject);
4050                    }
4051                },
4052                MoreExecutors.directExecutor());
4053    }
4054
4055    public void publishAvatar(
4056            final Account account,
4057            final Uri image,
4058            final boolean open,
4059            final OnAvatarPublication callback) {
4060
4061        final var connection = account.getXmppConnection();
4062        final var publicationFuture =
4063                connection.getManager(AvatarManager.class).uploadAndPublish(image, open);
4064
4065        Futures.addCallback(
4066                publicationFuture,
4067                new FutureCallback<>() {
4068                    @Override
4069                    public void onSuccess(final Void result) {
4070                        Log.d(Config.LOGTAG, "published avatar");
4071                        callback.onAvatarPublicationSucceeded();
4072                    }
4073
4074                    @Override
4075                    public void onFailure(@NonNull final Throwable t) {
4076                        Log.d(Config.LOGTAG, "avatar upload failed", t);
4077                        // TODO actually figure out what went wrong
4078                        callback.onAvatarPublicationFailed(
4079                                R.string.error_publish_avatar_server_reject);
4080                    }
4081                },
4082                MoreExecutors.directExecutor());
4083    }
4084
4085    public ListenableFuture<Void> checkForAvatar(final Account account) {
4086        final var connection = account.getXmppConnection();
4087        return connection
4088                .getManager(AvatarManager.class)
4089                .fetchAndStore(account.getJid().asBareJid());
4090    }
4091
4092    public void notifyAccountAvatarHasChanged(final Account account) {
4093        final XmppConnection connection = account.getXmppConnection();
4094        // this was bookmark conversion for a bit which doesn't make sense
4095        if (connection.getManager(AvatarManager.class).hasPepToVCardConversion()) {
4096            Log.d(
4097                    Config.LOGTAG,
4098                    account.getJid().asBareJid()
4099                            + ": avatar changed. resending presence to online group chats");
4100            for (Conversation conversation : conversations) {
4101                if (conversation.getAccount() == account
4102                        && conversation.getMode() == Conversational.MODE_MULTI) {
4103                    connection.getManager(MultiUserChatManager.class).resendPresence(conversation);
4104                }
4105            }
4106        }
4107    }
4108
4109    public void fetchVcard4(Account account, final Contact contact, final Consumer<Element> callback) {
4110        final var packet = this.mIqGenerator.retrieveVcard4(contact.getJid());
4111        sendIqPacket(account, packet, (result) -> {
4112            if (result.getType() == Iq.Type.RESULT) {
4113                final Element item = IqParser.getItem(result);
4114                if (item != null) {
4115                    final Element vcard4 = item.findChild("vcard", Namespace.VCARD4);
4116                    if (vcard4 != null) {
4117                        if (callback != null) {
4118                            callback.accept(vcard4);
4119                        }
4120                        return;
4121                    }
4122                }
4123            } else {
4124                Element error = result.findChild("error");
4125                if (error == null) {
4126                    Log.d(Config.LOGTAG, "fetchVcard4 (server error)");
4127                } else {
4128                    Log.d(Config.LOGTAG, "fetchVcard4 " + error.toString());
4129                }
4130            }
4131            if (callback != null) {
4132                callback.accept(null);
4133            }
4134
4135        });
4136    }
4137
4138    public void updateConversation(final Conversation conversation) {
4139        mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation));
4140    }
4141
4142    public void reconnectAccount(
4143            final Account account, final boolean force, final boolean interactive) {
4144        synchronized (account) {
4145            final XmppConnection connection = account.getXmppConnection();
4146            final boolean hasInternet = hasInternetConnection();
4147            if (account.isConnectionEnabled() && hasInternet) {
4148                if (!force) {
4149                    disconnect(account, false);
4150                }
4151                Thread thread = new Thread(connection);
4152                connection.setInteractive(interactive);
4153                connection.prepareNewConnection();
4154                connection.interrupt();
4155                thread.start();
4156                scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
4157            } else {
4158                disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
4159                connection.getManager(RosterManager.class).clearPresences();
4160                connection.resetEverything();
4161                final AxolotlService axolotlService = account.getAxolotlService();
4162                if (axolotlService != null) {
4163                    axolotlService.resetBrokenness();
4164                }
4165                if (!hasInternet) {
4166                    // TODO should this go via XmppConnection.setStatusAndTriggerProcessor()?
4167                    account.setStatus(Account.State.NO_INTERNET);
4168                }
4169            }
4170        }
4171    }
4172
4173    public void reconnectAccountInBackground(final Account account) {
4174        new Thread(() -> reconnectAccount(account, false, true)).start();
4175    }
4176
4177    public void invite(final Conversation conversation, final Jid contact) {
4178        final var account = conversation.getAccount();
4179        account.getXmppConnection()
4180                .getManager(MultiUserChatManager.class)
4181                .invite(conversation, contact);
4182    }
4183
4184    public void directInvite(Conversation conversation, Jid jid) {
4185        final var account = conversation.getAccount();
4186        account.getXmppConnection()
4187                .getManager(MultiUserChatManager.class)
4188                .directInvite(conversation, jid);
4189    }
4190
4191    public void resetSendingToWaiting(Account account) {
4192        for (Conversation conversation : getConversations()) {
4193            if (conversation.getAccount() == account) {
4194                conversation.findUnsentTextMessages(
4195                        message -> markMessage(message, Message.STATUS_WAITING));
4196            }
4197        }
4198    }
4199
4200    public Message markMessage(
4201            final Account account, final Jid recipient, final String uuid, final int status) {
4202        return markMessage(account, recipient, uuid, status, null);
4203    }
4204
4205    public Message markMessage(
4206            final Account account,
4207            final Jid recipient,
4208            final String uuid,
4209            final int status,
4210            String errorMessage) {
4211        if (uuid == null) {
4212            return null;
4213        }
4214        for (Conversation conversation : getConversations()) {
4215            if (conversation.getJid().asBareJid().equals(recipient)
4216                    && conversation.getAccount() == account) {
4217                final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
4218                if (message != null) {
4219                    markMessage(message, status, errorMessage);
4220                }
4221                return message;
4222            }
4223        }
4224        return null;
4225    }
4226
4227    public boolean markMessage(
4228            final Conversation conversation,
4229            final String uuid,
4230            final int status,
4231            final String serverMessageId) {
4232        return markMessage(conversation, uuid, status, serverMessageId, null, null, null, null, null);
4233    }
4234
4235    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) {
4236        if (uuid == null) {
4237            return false;
4238        } else {
4239            final Message message = conversation.findSentMessageWithUuid(uuid);
4240            if (message != null) {
4241                if (message.getServerMsgId() == null) {
4242                    message.setServerMsgId(serverMessageId);
4243                }
4244                if (message.getEncryption() == Message.ENCRYPTION_NONE && (body != null || html != null || subject != null || thread != null || attachments != null)) {
4245                    message.setBody(body.content);
4246                    if (body.count > 1) {
4247                        message.setBodyLanguage(body.language);
4248                    }
4249                    message.setHtml(html);
4250                    message.setSubject(subject);
4251                    message.setThread(thread);
4252                    if (attachments != null && attachments.isEmpty()) {
4253                        message.setRelativeFilePath(null);
4254                        message.resetFileParams();
4255                    }
4256                    markMessage(message, status, null, true);
4257                } else {
4258                    markMessage(message, status);
4259                }
4260                return true;
4261            } else {
4262                return false;
4263            }
4264        }
4265    }
4266
4267    public void markMessage(Message message, int status) {
4268        markMessage(message, status, null);
4269    }
4270
4271    public void markMessage(final Message message, final int status, final String errorMessage) {
4272        markMessage(message, status, errorMessage, false);
4273    }
4274
4275    public void markMessage(
4276            final Message message,
4277            final int status,
4278            final String errorMessage,
4279            final boolean includeBody) {
4280        final int oldStatus = message.getStatus();
4281        if (status == Message.STATUS_SEND_FAILED
4282                && (oldStatus == Message.STATUS_SEND_RECEIVED
4283                        || oldStatus == Message.STATUS_SEND_DISPLAYED)) {
4284            return;
4285        }
4286        if (status == Message.STATUS_SEND_RECEIVED && oldStatus == Message.STATUS_SEND_DISPLAYED) {
4287            return;
4288        }
4289        message.setErrorMessage(errorMessage);
4290        message.setStatus(status);
4291        databaseBackend.updateMessage(message, includeBody);
4292        updateConversationUi();
4293        if (oldStatus != status && status == Message.STATUS_SEND_FAILED) {
4294            mNotificationService.pushFailedDelivery(message);
4295        }
4296    }
4297
4298    public SharedPreferences getPreferences() {
4299        return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
4300    }
4301
4302    public long getAutomaticMessageDeletionDate() {
4303        final long timeout =
4304                getLongPreference(
4305                        AppSettings.AUTOMATIC_MESSAGE_DELETION,
4306                        R.integer.automatic_message_deletion);
4307        return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000));
4308    }
4309
4310    public long getLongPreference(String name, @IntegerRes int res) {
4311        long defaultValue = getResources().getInteger(res);
4312        try {
4313            return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue)));
4314        } catch (NumberFormatException e) {
4315            return defaultValue;
4316        }
4317    }
4318
4319    public boolean getBooleanPreference(String name, int res) {
4320        return getPreferences().getBoolean(name, getResources().getBoolean(res));
4321    }
4322
4323    public String getStringPreference(String name, int res) {
4324        return getPreferences().getString(name, getResources().getString(res));
4325    }
4326
4327    public boolean confirmMessages() {
4328        return appSettings.isConfirmMessages();
4329    }
4330
4331    public boolean allowMessageCorrection() {
4332        return appSettings.isAllowMessageCorrection();
4333    }
4334
4335    public boolean useTorToConnect() {
4336        return appSettings.isUseTor();
4337    }
4338
4339    public int unreadCount() {
4340        int count = 0;
4341        for (Conversation conversation : getConversations()) {
4342            count += conversation.unreadCount(this);
4343        }
4344        return count;
4345    }
4346
4347    private <T> List<T> threadSafeList(Set<T> set) {
4348        synchronized (LISTENER_LOCK) {
4349            return set.isEmpty() ? Collections.emptyList() : new ArrayList<>(set);
4350        }
4351    }
4352
4353    public void showErrorToastInUi(int resId) {
4354        for (OnShowErrorToast listener : threadSafeList(this.mOnShowErrorToasts)) {
4355            listener.onShowErrorToast(resId);
4356        }
4357    }
4358
4359    public void updateConversationUi() {
4360        updateConversationUi(false);
4361    }
4362
4363    public void updateConversationUi(boolean newCaps) {
4364        for (OnConversationUpdate listener : threadSafeList(this.mOnConversationUpdates)) {
4365            listener.onConversationUpdate(newCaps);
4366        }
4367    }
4368
4369    public void notifyJingleRtpConnectionUpdate(
4370            final Account account,
4371            final Jid with,
4372            final String sessionId,
4373            final RtpEndUserState state) {
4374        for (OnJingleRtpConnectionUpdate listener :
4375                threadSafeList(this.onJingleRtpConnectionUpdate)) {
4376            listener.onJingleRtpConnectionUpdate(account, with, sessionId, state);
4377        }
4378    }
4379
4380    public void notifyJingleRtpConnectionUpdate(
4381            CallIntegration.AudioDevice selectedAudioDevice,
4382            Set<CallIntegration.AudioDevice> availableAudioDevices) {
4383        for (OnJingleRtpConnectionUpdate listener :
4384                threadSafeList(this.onJingleRtpConnectionUpdate)) {
4385            listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
4386        }
4387    }
4388
4389    public void updateAccountUi() {
4390        for (final OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) {
4391            listener.onAccountUpdate();
4392        }
4393    }
4394
4395    public void updateRosterUi(final UpdateRosterReason reason) {
4396        if (reason == UpdateRosterReason.PRESENCE) throw new IllegalArgumentException("PRESENCE must also come with a contact");
4397        updateRosterUi(reason, null);
4398    }
4399
4400    public void updateRosterUi(final UpdateRosterReason reason, final Contact contact) {
4401        for (OnRosterUpdate listener : threadSafeList(this.mOnRosterUpdates)) {
4402            listener.onRosterUpdate(reason, contact);
4403        }
4404    }
4405
4406    public boolean displayCaptchaRequest(
4407            final Account account,
4408            final im.conversations.android.xmpp.model.data.Data data,
4409            final Bitmap captcha) {
4410        if (mOnCaptchaRequested.isEmpty()) {
4411            return false;
4412        }
4413        final var metrics = getApplicationContext().getResources().getDisplayMetrics();
4414        Bitmap scaled =
4415                Bitmap.createScaledBitmap(
4416                        captcha,
4417                        (int) (captcha.getWidth() * metrics.scaledDensity),
4418                        (int) (captcha.getHeight() * metrics.scaledDensity),
4419                        false);
4420        for (final OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
4421            listener.onCaptchaRequested(account, data, scaled);
4422        }
4423        return true;
4424    }
4425
4426    public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
4427        for (OnUpdateBlocklist listener : threadSafeList(this.mOnUpdateBlocklist)) {
4428            listener.OnUpdateBlocklist(status);
4429        }
4430    }
4431
4432    public void updateMucRosterUi() {
4433        for (OnMucRosterUpdate listener : threadSafeList(this.mOnMucRosterUpdate)) {
4434            listener.onMucRosterUpdate();
4435        }
4436    }
4437
4438    public void keyStatusUpdated(AxolotlService.FetchStatus report) {
4439        for (OnKeyStatusUpdated listener : threadSafeList(this.mOnKeyStatusUpdated)) {
4440            listener.onKeyStatusUpdated(report);
4441        }
4442    }
4443
4444    public Account findAccountByJid(final Jid jid) {
4445        for (final Account account : this.accounts) {
4446            if (account.getJid().asBareJid().equals(jid.asBareJid())) {
4447                return account;
4448            }
4449        }
4450        return null;
4451    }
4452
4453    public Account findAccountByUuid(final String uuid) {
4454        for (Account account : this.accounts) {
4455            if (account.getUuid().equals(uuid)) {
4456                return account;
4457            }
4458        }
4459        return null;
4460    }
4461
4462    public Conversation findConversationByUuid(String uuid) {
4463        for (Conversation conversation : getConversations()) {
4464            if (conversation.getUuid().equals(uuid)) {
4465                return conversation;
4466            }
4467        }
4468        return null;
4469    }
4470
4471    public Conversation findUniqueConversationByJid(XmppUri xmppUri) {
4472        List<Conversation> findings = new ArrayList<>();
4473        for (Conversation c : getConversations()) {
4474            if (c.getAccount().isEnabled()
4475                    && c.getJid().asBareJid().equals(xmppUri.getJid().asBareJid())
4476                    && ((c.getMode() == Conversational.MODE_MULTI)
4477                            == xmppUri.isAction(XmppUri.ACTION_JOIN))) {
4478                findings.add(c);
4479            }
4480        }
4481        return findings.size() == 1 ? findings.get(0) : null;
4482    }
4483
4484    public boolean markRead(final Conversation conversation, boolean dismiss) {
4485        return markRead(conversation, null, dismiss).size() > 0;
4486    }
4487
4488    public void markRead(final Conversation conversation) {
4489        markRead(conversation, null, true);
4490    }
4491
4492    public List<Message> markRead(
4493            final Conversation conversation, String upToUuid, boolean dismiss) {
4494        if (dismiss) {
4495            mNotificationService.clear(conversation);
4496        }
4497        final List<Message> readMessages = conversation.markRead(upToUuid);
4498        if (readMessages.size() > 0) {
4499            Runnable runnable =
4500                    () -> {
4501                        for (Message message : readMessages) {
4502                            databaseBackend.updateMessage(message, false);
4503                        }
4504                    };
4505            mDatabaseWriterExecutor.execute(runnable);
4506            updateConversationUi();
4507            updateUnreadCountBadge();
4508            return readMessages;
4509        } else {
4510            return readMessages;
4511        }
4512    }
4513
4514    public void markNotificationDismissed(final List<Message> messages) {
4515        Runnable runnable = () -> {
4516            for (final var message : messages) {
4517                message.markNotificationDismissed();
4518                databaseBackend.updateMessage(message, false);
4519            }
4520        };
4521        mDatabaseWriterExecutor.execute(runnable);
4522    }
4523
4524    public synchronized void updateUnreadCountBadge() {
4525        int count = unreadCount();
4526        if (unreadCount != count) {
4527            Log.d(Config.LOGTAG, "update unread count to " + count);
4528            if (count > 0) {
4529                ShortcutBadger.applyCount(getApplicationContext(), count);
4530            } else {
4531                ShortcutBadger.removeCount(getApplicationContext());
4532            }
4533            unreadCount = count;
4534        }
4535    }
4536
4537    public void sendReadMarker(final Conversation conversation, final String upToUuid) {
4538        final boolean isPrivateAndNonAnonymousMuc =
4539                conversation.getMode() == Conversation.MODE_MULTI
4540                        && conversation.isPrivateAndNonAnonymous();
4541        final List<Message> readMessages = this.markRead(conversation, upToUuid, true);
4542        if (readMessages.isEmpty()) {
4543            return;
4544        }
4545        final var account = conversation.getAccount();
4546        final var connection = account.getXmppConnection();
4547        updateConversationUi();
4548        final var last =
4549                Iterables.getLast(
4550                        Collections2.filter(
4551                                readMessages,
4552                                m ->
4553                                        !m.isPrivateMessage()
4554                                                && m.getStatus() == Message.STATUS_RECEIVED),
4555                        null);
4556        if (last == null) {
4557            return;
4558        }
4559
4560        final boolean sendDisplayedMarker =
4561                confirmMessages()
4562                        && (last.trusted() || isPrivateAndNonAnonymousMuc)
4563                        && last.getRemoteMsgId() != null
4564                        && (last.markable || isPrivateAndNonAnonymousMuc);
4565        final boolean serverAssist =
4566                connection != null && connection.getFeatures().mdsServerAssist();
4567
4568        final String stanzaId = last.getServerMsgId();
4569
4570        if (sendDisplayedMarker && serverAssist) {
4571            final var mdsDisplayed =
4572                    MessageDisplayedSynchronizationManager.displayed(stanzaId, conversation);
4573            final var packet = mMessageGenerator.confirm(last);
4574            packet.addChild(mdsDisplayed);
4575            if (!last.isPrivateMessage()) {
4576                packet.setTo(packet.getTo().asBareJid());
4577            }
4578            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server assisted " + packet);
4579            this.sendMessagePacket(account, packet);
4580        } else {
4581            publishMds(last);
4582            // read markers will be sent after MDS to flush the CSI stanza queue
4583            if (sendDisplayedMarker) {
4584                Log.d(
4585                        Config.LOGTAG,
4586                        conversation.getAccount().getJid().asBareJid()
4587                                + ": sending displayed marker to "
4588                                + last.getCounterpart().toString());
4589                final var packet = mMessageGenerator.confirm(last);
4590                this.sendMessagePacket(account, packet);
4591            }
4592        }
4593    }
4594
4595    private void publishMds(@Nullable final Message message) {
4596        final String stanzaId = message == null ? null : message.getServerMsgId();
4597        if (Strings.isNullOrEmpty(stanzaId)) {
4598            return;
4599        }
4600        final Conversation conversation;
4601        final var conversational = message.getConversation();
4602        if (conversational instanceof Conversation c) {
4603            conversation = c;
4604        } else {
4605            return;
4606        }
4607        final var account = conversation.getAccount();
4608        final var connection = account.getXmppConnection();
4609        if (connection == null || !connection.getFeatures().mds()) {
4610            return;
4611        }
4612        final Jid itemId;
4613        if (message.isPrivateMessage()) {
4614            itemId = message.getCounterpart();
4615        } else {
4616            itemId = conversation.getJid().asBareJid();
4617        }
4618        Log.d(Config.LOGTAG, "publishing mds for " + itemId + "/" + stanzaId);
4619        final var displayed =
4620                MessageDisplayedSynchronizationManager.displayed(stanzaId, conversation);
4621        connection
4622                .getManager(MessageDisplayedSynchronizationManager.class)
4623                .publish(itemId, displayed);
4624    }
4625
4626    public boolean sendReactions(final Message message, final Collection<String> reactions) {
4627        if (message.isPrivateMessage()) throw new IllegalArgumentException("Reactions to PM not implemented");
4628        if (message.getConversation() instanceof Conversation conversation) {
4629            final var isPrivateMessage = message.isPrivateMessage();
4630            final Jid reactTo;
4631            final boolean typeGroupChat;
4632            final String reactToId;
4633            final Collection<Reaction> combinedReactions;
4634            final var newReactions = new HashSet<>(reactions);
4635            newReactions.removeAll(message.getAggregatedReactions().ourReactions);
4636            if (conversation.getMode() == Conversational.MODE_MULTI && !isPrivateMessage) {
4637                final var mucOptions = conversation.getMucOptions();
4638                if (!mucOptions.participating()) {
4639                    Log.e(Config.LOGTAG, "not participating in MUC");
4640                    return false;
4641                }
4642                final var self = mucOptions.getSelf();
4643                final String occupantId = self.getOccupantId();
4644                if (Strings.isNullOrEmpty(occupantId)) {
4645                    Log.e(Config.LOGTAG, "occupant id not found for reaction in MUC");
4646                    return false;
4647                }
4648                final var existingRaw =
4649                        ImmutableSet.copyOf(
4650                                Collections2.transform(message.getReactions(), r -> r.reaction));
4651                final var reactionsAsExistingVariants =
4652                        ImmutableSet.copyOf(
4653                                Collections2.transform(
4654                                        reactions, r -> Emoticons.existingVariant(r, existingRaw)));
4655                if (!reactions.equals(reactionsAsExistingVariants)) {
4656                    Log.d(Config.LOGTAG, "modified reactions to existing variants");
4657                }
4658                reactToId = message.getServerMsgId();
4659                reactTo = conversation.getJid().asBareJid();
4660                typeGroupChat = true;
4661                combinedReactions =
4662                        Reaction.withMine(
4663                                message.getReactions(),
4664                                reactionsAsExistingVariants,
4665                                false,
4666                                self.getFullJid(),
4667                                conversation.getAccount().getJid(),
4668                                occupantId,
4669                                null);
4670            } else {
4671                if (message.isCarbon() || message.getStatus() == Message.STATUS_RECEIVED) {
4672                    reactToId = message.getRemoteMsgId();
4673                } else {
4674                    reactToId = message.getUuid();
4675                }
4676                typeGroupChat = false;
4677                if (isPrivateMessage) {
4678                    reactTo = message.getCounterpart();
4679                } else {
4680                    reactTo = conversation.getJid().asBareJid();
4681                }
4682                combinedReactions =
4683                        Reaction.withFrom(
4684                                message.getReactions(),
4685                                reactions,
4686                                false,
4687                                conversation.getAccount().getJid(),
4688                                null);
4689            }
4690            if (reactTo == null || Strings.isNullOrEmpty(reactToId)) {
4691                Log.e(Config.LOGTAG, "could not find id to react to");
4692                return false;
4693            }
4694
4695            final var packet =
4696                    mMessageGenerator.reaction(reactTo, typeGroupChat, message, reactToId, reactions);
4697
4698            final var quote = QuoteHelper.quote(MessageUtils.prepareQuote(message, 1, 2)) + "\n\n";
4699            final var body  = quote + String.join(" ", newReactions);
4700            if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && newReactions.size() > 0) {
4701                FILE_ATTACHMENT_EXECUTOR.execute(() -> {
4702                    XmppAxolotlMessage axolotlMessage = conversation.getAccount().getAxolotlService().encrypt(body, conversation);
4703                    packet.setAxolotlMessage(axolotlMessage.toElement());
4704                    packet.addChild("encryption", "urn:xmpp:eme:0")
4705                        .setAttribute("name", "OMEMO")
4706                        .setAttribute("namespace", AxolotlService.PEP_PREFIX);
4707                    sendMessagePacket(conversation.getAccount(), packet);
4708                    message.setReactions(combinedReactions);
4709                    updateMessage(message, false);
4710                });
4711            } else if (conversation.getNextEncryption() == Message.ENCRYPTION_NONE || newReactions.size() < 1) {
4712                if (newReactions.size() > 0) {
4713                    packet.setBody(body);
4714
4715                    packet.addChild("reply", "urn:xmpp:reply:0")
4716                        .setAttribute("to", message.getCounterpart())
4717                        .setAttribute("id", reactToId);
4718                    final var replyFallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reply:0");
4719                    replyFallback.addChild("body", "urn:xmpp:fallback:0")
4720                        .setAttribute("start", "0")
4721                        .setAttribute("end", "" + quote.codePointCount(0, quote.length()));
4722
4723                    final var fallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reactions:0");
4724                    fallback.addChild("body", "urn:xmpp:fallback:0");
4725                }
4726
4727                sendMessagePacket(conversation.getAccount(), packet);
4728                message.setReactions(combinedReactions);
4729                updateMessage(message, false);
4730            }
4731
4732            return true;
4733        } else {
4734            return false;
4735        }
4736    }
4737
4738    public MemorizingTrustManager getMemorizingTrustManager() {
4739        return this.mMemorizingTrustManager;
4740    }
4741
4742    public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
4743        this.mMemorizingTrustManager = trustManager;
4744    }
4745
4746    public void updateMemorizingTrustManager() {
4747        final MemorizingTrustManager trustManager;
4748        if (appSettings.isTrustSystemCAStore()) {
4749            trustManager = new MemorizingTrustManager(getApplicationContext());
4750        } else {
4751            trustManager = new MemorizingTrustManager(getApplicationContext(), null);
4752        }
4753        setMemorizingTrustManager(trustManager);
4754    }
4755
4756    public LruCache<String, Drawable> getDrawableCache() {
4757        return this.mDrawableCache;
4758    }
4759
4760    public Collection<String> getKnownHosts() {
4761        final Set<String> hosts = new HashSet<>();
4762        for (final Account account : getAccounts()) {
4763            hosts.add(account.getServer());
4764            for (final Contact contact : account.getRoster().getContacts()) {
4765                if (contact.showInRoster()) {
4766                    final String server = contact.getServer();
4767                    if (server != null) {
4768                        hosts.add(server);
4769                    }
4770                }
4771            }
4772        }
4773        if (Config.QUICKSY_DOMAIN != null) {
4774            hosts.remove(
4775                    Config.QUICKSY_DOMAIN
4776                            .toString()); // we only want to show this when we type a e164
4777            // number
4778        }
4779        if (Config.MAGIC_CREATE_DOMAIN != null) {
4780            hosts.add(Config.MAGIC_CREATE_DOMAIN);
4781        }
4782        hosts.add("chat.above.im");
4783        return hosts;
4784    }
4785
4786    public Collection<String> getKnownConferenceHosts() {
4787        final var builder = new ImmutableSet.Builder<Jid>();
4788        for (final Account account : accounts) {
4789            final var connection = account.getXmppConnection();
4790            builder.addAll(connection.getManager(MultiUserChatManager.class).getServices());
4791            for (final var bookmark : account.getBookmarks()) {
4792                final Jid jid = bookmark.getJid();
4793                final Jid domain = jid == null ? null : jid.getDomain();
4794                if (domain == null) {
4795                    continue;
4796                }
4797                builder.add(domain);
4798            }
4799        }
4800        return Collections2.transform(builder.build(), Jid::toString);
4801    }
4802
4803    public void sendMessagePacket(
4804            final Account account,
4805            final im.conversations.android.xmpp.model.stanza.Message packet) {
4806        final XmppConnection connection = account.getXmppConnection();
4807        if (connection != null) {
4808            connection.sendMessagePacket(packet);
4809        }
4810    }
4811
4812    public ListenableFuture<Iq> sendIqPacket(final Account account, final Iq request) {
4813        final XmppConnection connection = account.getXmppConnection();
4814        if (connection == null) {
4815            return Futures.immediateFailedFuture(new TimeoutException());
4816        }
4817        return connection.sendIqPacket(request);
4818    }
4819
4820    public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback) {
4821        sendIqPacket(account, packet, callback, null);
4822    }
4823
4824    public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback, Long timeout) {
4825        final XmppConnection connection = account.getXmppConnection();
4826        if (connection != null) {
4827            connection.sendIqPacket(packet, callback, timeout);
4828        } else if (callback != null) {
4829            callback.accept(Iq.TIMEOUT);
4830        }
4831    }
4832
4833    private void deactivateGracePeriod() {
4834        for (Account account : getAccounts()) {
4835            account.deactivateGracePeriod();
4836        }
4837    }
4838
4839    public void refreshAllPresences() {
4840        final boolean includeIdleTimestamp =
4841                checkListeners() && appSettings.isBroadcastLastActivity();
4842        for (final var account : getAccounts()) {
4843            if (account.isConnectionEnabled()) {
4844                account.getXmppConnection()
4845                        .getManager(PresenceManager.class)
4846                        .available(includeIdleTimestamp);
4847            }
4848        }
4849    }
4850
4851    private void refreshAllFcmTokens() {
4852        for (Account account : getAccounts()) {
4853            if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
4854                mPushManagementService.registerPushTokenOnServer(account);
4855            }
4856        }
4857    }
4858
4859    public MessageGenerator getMessageGenerator() {
4860        return this.mMessageGenerator;
4861    }
4862
4863    public IqGenerator getIqGenerator() {
4864        return this.mIqGenerator;
4865    }
4866
4867    public JingleConnectionManager getJingleConnectionManager() {
4868        return this.mJingleConnectionManager;
4869    }
4870
4871    public boolean hasJingleRtpConnection(final Account account) {
4872        return this.mJingleConnectionManager.hasJingleRtpConnection(account);
4873    }
4874
4875    public MessageArchiveService getMessageArchiveService() {
4876        return this.mMessageArchiveService;
4877    }
4878
4879    public QuickConversationsService getQuickConversationsService() {
4880        return this.mQuickConversationsService;
4881    }
4882
4883    public List<Contact> findContacts(Jid jid, String accountJid) {
4884        ArrayList<Contact> contacts = new ArrayList<>();
4885        for (Account account : getAccounts()) {
4886            if ((account.isEnabled() || accountJid != null)
4887                    && (accountJid == null
4888                            || accountJid.equals(account.getJid().asBareJid().toString()))) {
4889                Contact contact = account.getRoster().getContactFromContactList(jid);
4890                if (contact != null) {
4891                    contacts.add(contact);
4892                }
4893            }
4894        }
4895        return contacts;
4896    }
4897
4898    public Conversation findFirstMuc(Jid jid) {
4899        return findFirstMuc(jid, null);
4900    }
4901
4902    public Conversation findFirstMuc(Jid jid, String accountJid) {
4903        for (Conversation conversation : getConversations()) {
4904            if ((conversation.getAccount().isEnabled() || accountJid != null)
4905                    && (accountJid == null || accountJid.equals(conversation.getAccount().getJid().asBareJid().toString()))
4906                    && conversation.getJid().asBareJid().equals(jid.asBareJid()) && conversation.getMode() == Conversation.MODE_MULTI) {
4907                return conversation;
4908            }
4909        }
4910        return null;
4911    }
4912
4913    public NotificationService getNotificationService() {
4914        return this.mNotificationService;
4915    }
4916
4917    public HttpConnectionManager getHttpConnectionManager() {
4918        return this.mHttpConnectionManager;
4919    }
4920
4921    public void resendFailedMessages(final Message message, final boolean forceP2P) {
4922        message.setTime(System.currentTimeMillis());
4923        markMessage(message, Message.STATUS_WAITING);
4924        this.sendMessage(message, true, false, false, forceP2P, null);
4925        if (message.getConversation() instanceof Conversation c) {
4926            c.sort();
4927        }
4928        updateConversationUi();
4929    }
4930
4931    public void clearConversationHistory(final Conversation conversation) {
4932        final long clearDate;
4933        final String reference;
4934        if (conversation.countMessages() > 0) {
4935            Message latestMessage = conversation.getLatestMessage();
4936            clearDate = latestMessage.getTimeSent() + 1000;
4937            reference = latestMessage.getServerMsgId();
4938        } else {
4939            clearDate = System.currentTimeMillis();
4940            reference = null;
4941        }
4942        conversation.clearMessages();
4943        conversation.setHasMessagesLeftOnServer(false); // avoid messages getting loaded through mam
4944        conversation.setLastClearHistory(clearDate, reference);
4945        Runnable runnable =
4946                () -> {
4947                    databaseBackend.deleteMessagesInConversation(conversation);
4948                    databaseBackend.updateConversation(conversation);
4949                };
4950        mDatabaseWriterExecutor.execute(runnable);
4951    }
4952
4953    public boolean sendBlockRequest(
4954            final Blockable blockable, final boolean reportSpam, final String serverMsgId) {
4955        final var account = blockable.getAccount();
4956        final var connection = account.getXmppConnection();
4957        return connection
4958                .getManager(BlockingManager.class)
4959                .block(blockable, reportSpam, serverMsgId);
4960    }
4961
4962    public boolean removeBlockedConversations(final Account account, final Jid blockedJid) {
4963        boolean removed = false;
4964        synchronized (this.conversations) {
4965            boolean domainJid = blockedJid.getLocal() == null;
4966            for (Conversation conversation : this.conversations) {
4967                boolean jidMatches =
4968                        (domainJid
4969                                        && blockedJid
4970                                                .getDomain()
4971                                                .equals(conversation.getJid().getDomain()))
4972                                || blockedJid.equals(conversation.getJid().asBareJid());
4973                if (conversation.getAccount() == account
4974                        && conversation.getMode() == Conversation.MODE_SINGLE
4975                        && jidMatches) {
4976                    this.conversations.remove(conversation);
4977                    getMessageArchiveService().kill(conversation);
4978                    markRead(conversation);
4979                    conversation.setStatus(Conversation.STATUS_ARCHIVED);
4980                    Log.d(
4981                            Config.LOGTAG,
4982                            account.getJid().asBareJid()
4983                                    + ": archiving conversation "
4984                                    + conversation.getJid().asBareJid()
4985                                    + " because jid was blocked");
4986                    updateConversation(conversation);
4987                    removed = true;
4988                }
4989            }
4990        }
4991        return removed;
4992    }
4993
4994    public void sendUnblockRequest(final Blockable blockable) {
4995        final var account = blockable.getAccount();
4996        final var connection = account.getXmppConnection();
4997        connection.getManager(BlockingManager.class).unblock(blockable);
4998    }
4999
5000    public void publishDisplayName(final Account account) {
5001        final var connection = account.getXmppConnection();
5002        final String displayName = account.getDisplayName();
5003        mAvatarService.clear(account);
5004        final var future = connection.getManager(NickManager.class).publish(displayName);
5005        Futures.addCallback(
5006                future,
5007                new FutureCallback<Void>() {
5008                    @Override
5009                    public void onSuccess(Void result) {
5010                        Log.d(
5011                                Config.LOGTAG,
5012                                account.getJid().asBareJid() + ": published User Nick");
5013                    }
5014
5015                    @Override
5016                    public void onFailure(@NonNull Throwable t) {
5017                        Log.d(Config.LOGTAG, "could not publish User Nick", t);
5018                    }
5019                },
5020                MoreExecutors.directExecutor());
5021    }
5022
5023    public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) {
5024        final var request = new Iq(input == null ? Iq.Type.GET : Iq.Type.SET);
5025        request.setTo(jid);
5026        Element query = request.addChild("query");
5027        query.setAttribute("xmlns", "jabber:iq:gateway");
5028        if (input != null) {
5029            Element prompt = query.addChild("prompt");
5030            prompt.setContent(input);
5031        }
5032        sendIqPacket(account, request, packet -> {
5033            if (packet.getType() == Iq.Type.RESULT) {
5034                callback.onGatewayResult(packet.findChild("query").findChildContent(input == null ? "prompt" : "jid"), null);
5035            } else {
5036                Element error = packet.findChild("error");
5037                callback.onGatewayResult(null, error == null ? null : error.findChildContent("text"));
5038            }
5039        });
5040    }
5041
5042    public void fetchMamPreferences(final Account account, final OnMamPreferencesFetched callback) {
5043        final MessageArchiveService.Version version = MessageArchiveService.Version.get(account);
5044        final Iq request = new Iq(Iq.Type.GET);
5045        request.addChild("prefs", version.namespace);
5046        sendIqPacket(
5047                account,
5048                request,
5049                (packet) -> {
5050                    final Element prefs = packet.findChild("prefs", version.namespace);
5051                    if (packet.getType() == Iq.Type.RESULT && prefs != null) {
5052                        account.setMamPrefs(prefs);
5053                        callback.onPreferencesFetched(prefs);
5054                    } else {
5055                        callback.onPreferencesFetchFailed();
5056                    }
5057                });
5058    }
5059
5060    public PushManagementService getPushManagementService() {
5061        return mPushManagementService;
5062    }
5063
5064    public void changeStatus(
5065            final Account account, final PresenceTemplate template, final String signature) {
5066        if (!template.getStatusMessage().isEmpty()) {
5067            databaseBackend.insertPresenceTemplate(template);
5068        }
5069        account.setPgpSignature(signature);
5070        account.setPresenceStatus(template.getStatus());
5071        account.setPresenceStatusMessage(template.getStatusMessage());
5072        databaseBackend.updateAccount(account);
5073        account.getXmppConnection().getManager(PresenceManager.class).available();
5074    }
5075
5076    public List<PresenceTemplate> getPresenceTemplates(Account account) {
5077        List<PresenceTemplate> templates = databaseBackend.getPresenceTemplates();
5078        for (PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) {
5079            if (!templates.contains(template)) {
5080                templates.add(0, template);
5081            }
5082        }
5083        return templates;
5084    }
5085
5086    public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
5087        boolean performedVerification = false;
5088        final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
5089        for (XmppUri.Fingerprint fp : fingerprints) {
5090            if (fp.type == XmppUri.FingerprintType.OMEMO) {
5091                String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
5092                FingerprintStatus fingerprintStatus =
5093                        axolotlService.getFingerprintTrust(fingerprint);
5094                if (fingerprintStatus != null) {
5095                    if (!fingerprintStatus.isVerified()) {
5096                        performedVerification = true;
5097                        axolotlService.setFingerprintTrust(
5098                                fingerprint, fingerprintStatus.toVerified());
5099                    }
5100                } else {
5101                    axolotlService.preVerifyFingerprint(contact, fingerprint);
5102                }
5103            }
5104        }
5105        return performedVerification;
5106    }
5107
5108    public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
5109        final AxolotlService axolotlService = account.getAxolotlService();
5110        boolean verifiedSomething = false;
5111        for (XmppUri.Fingerprint fp : fingerprints) {
5112            if (fp.type == XmppUri.FingerprintType.OMEMO) {
5113                String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
5114                Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint);
5115                FingerprintStatus fingerprintStatus =
5116                        axolotlService.getFingerprintTrust(fingerprint);
5117                if (fingerprintStatus != null) {
5118                    if (!fingerprintStatus.isVerified()) {
5119                        axolotlService.setFingerprintTrust(
5120                                fingerprint, fingerprintStatus.toVerified());
5121                        verifiedSomething = true;
5122                    }
5123                } else {
5124                    axolotlService.preVerifyFingerprint(account, fingerprint);
5125                    verifiedSomething = true;
5126                }
5127            }
5128        }
5129        return verifiedSomething;
5130    }
5131
5132    public ShortcutService getShortcutService() {
5133        return mShortcutService;
5134    }
5135
5136    public void pushMamPreferences(Account account, Element prefs) {
5137        final Iq set = new Iq(Iq.Type.SET);
5138        set.addChild(prefs);
5139        account.setMamPrefs(prefs);
5140        sendIqPacket(account, set, null);
5141    }
5142
5143    public void evictPreview(File f) {
5144        if (f == null) return;
5145
5146        if (mDrawableCache.remove(f.getAbsolutePath()) != null) {
5147            Log.d(Config.LOGTAG, "deleted cached preview");
5148        }
5149    }
5150
5151    public void evictPreview(String uuid) {
5152        if (mDrawableCache.remove(uuid) != null) {
5153            Log.d(Config.LOGTAG, "deleted cached preview");
5154        }
5155    }
5156
5157    public long getLastActivity() {
5158        return this.mLastActivity;
5159    }
5160
5161    public interface OnMamPreferencesFetched {
5162        void onPreferencesFetched(Element prefs);
5163
5164        void onPreferencesFetchFailed();
5165    }
5166
5167    public interface OnAccountCreated {
5168        void onAccountCreated(Account account);
5169
5170        void informUser(int r);
5171    }
5172
5173    public interface JumpToMessageListener {
5174        void onSuccess();
5175        void onNotFound();
5176    }
5177
5178    public interface OnMoreMessagesLoaded {
5179        void onMoreMessagesLoaded(int count, Conversation conversation);
5180
5181        void informUser(int r);
5182    }
5183
5184    public interface OnAffiliationChanged {
5185        void onAffiliationChangedSuccessful(Jid jid);
5186
5187        void onAffiliationChangeFailed(Jid jid, int resId);
5188    }
5189
5190    public interface OnConversationUpdate {
5191        default void onConversationUpdate() { onConversationUpdate(false); }
5192        default void onConversationUpdate(boolean newCaps) { onConversationUpdate(); }
5193    }
5194
5195    public interface OnJingleRtpConnectionUpdate {
5196        void onJingleRtpConnectionUpdate(
5197                final Account account,
5198                final Jid with,
5199                final String sessionId,
5200                final RtpEndUserState state);
5201
5202        void onAudioDeviceChanged(
5203                CallIntegration.AudioDevice selectedAudioDevice,
5204                Set<CallIntegration.AudioDevice> availableAudioDevices);
5205    }
5206
5207    public interface OnAccountUpdate {
5208        void onAccountUpdate();
5209    }
5210
5211    public interface OnCaptchaRequested {
5212        void onCaptchaRequested(
5213                Account account,
5214                im.conversations.android.xmpp.model.data.Data data,
5215                Bitmap captcha);
5216    }
5217
5218    public interface OnRosterUpdate {
5219        void onRosterUpdate(final UpdateRosterReason reason, final Contact contact);
5220    }
5221
5222    public interface OnMucRosterUpdate {
5223        void onMucRosterUpdate();
5224    }
5225
5226    public interface OnConferenceConfigurationFetched {
5227        void onConferenceConfigurationFetched(Conversation conversation);
5228
5229        void onFetchFailed(Conversation conversation, String errorCondition);
5230    }
5231
5232    public interface OnConferenceJoined {
5233        void onConferenceJoined(Conversation conversation);
5234    }
5235
5236    public interface OnConfigurationPushed {
5237        void onPushSucceeded();
5238
5239        void onPushFailed();
5240    }
5241
5242    public interface OnShowErrorToast {
5243        void onShowErrorToast(int resId);
5244    }
5245
5246    public class XmppConnectionBinder extends Binder {
5247        public XmppConnectionService getService() {
5248            return XmppConnectionService.this;
5249        }
5250    }
5251
5252    private class InternalEventReceiver extends BroadcastReceiver {
5253
5254        @Override
5255        public void onReceive(final Context context, final Intent intent) {
5256            onStartCommand(intent, 0, 0);
5257        }
5258    }
5259
5260    private class RestrictedEventReceiver extends BroadcastReceiver {
5261
5262        private final Collection<String> allowedActions;
5263
5264        private RestrictedEventReceiver(final Collection<String> allowedActions) {
5265            this.allowedActions = allowedActions;
5266        }
5267
5268        @Override
5269        public void onReceive(final Context context, final Intent intent) {
5270            final String action = intent == null ? null : intent.getAction();
5271            if (allowedActions.contains(action)) {
5272                onStartCommand(intent, 0, 0);
5273            } else {
5274                Log.e(Config.LOGTAG, "restricting broadcast of event " + action);
5275            }
5276        }
5277    }
5278
5279    public static class OngoingCall {
5280        public final AbstractJingleConnection.Id id;
5281        public final Set<Media> media;
5282        public final boolean reconnecting;
5283
5284        public OngoingCall(
5285                AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
5286            this.id = id;
5287            this.media = media;
5288            this.reconnecting = reconnecting;
5289        }
5290
5291        @Override
5292        public boolean equals(Object o) {
5293            if (this == o) return true;
5294            if (o == null || getClass() != o.getClass()) return false;
5295            OngoingCall that = (OngoingCall) o;
5296            return reconnecting == that.reconnecting
5297                    && Objects.equal(id, that.id)
5298                    && Objects.equal(media, that.media);
5299        }
5300
5301        @Override
5302        public int hashCode() {
5303            return Objects.hashCode(id, media, reconnecting);
5304        }
5305    }
5306
5307    public static void toggleForegroundService(final XmppConnectionService service) {
5308        if (service == null) {
5309            return;
5310        }
5311        service.toggleForegroundService();
5312    }
5313
5314    public static void toggleForegroundService(final ConversationsActivity activity) {
5315        if (activity == null) {
5316            return;
5317        }
5318        toggleForegroundService(activity.xmppConnectionService);
5319    }
5320
5321    public static class BlockedMediaException extends Exception { }
5322
5323    public static enum UpdateRosterReason {
5324        INIT,
5325        AVATAR,
5326        PUSH,
5327        PRESENCE
5328    }
5329}