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