XmppConnectionService.java

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