XmppConnectionService.java

   1package eu.siacs.conversations.services;
   2
   3import static eu.siacs.conversations.utils.Compatibility.s;
   4import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
   5
   6import android.Manifest;
   7import android.annotation.SuppressLint;
   8import android.app.AlarmManager;
   9import android.app.KeyguardManager;
  10import android.app.Notification;
  11import android.app.NotificationManager;
  12import android.app.PendingIntent;
  13import android.app.Service;
  14import android.content.BroadcastReceiver;
  15import android.content.ComponentName;
  16import android.content.Context;
  17import android.content.Intent;
  18import android.content.IntentFilter;
  19import android.content.SharedPreferences;
  20import android.content.pm.PackageManager;
  21import android.content.pm.ServiceInfo;
  22import android.database.ContentObserver;
  23import android.graphics.Bitmap;
  24import android.graphics.drawable.AnimatedImageDrawable;
  25import android.graphics.drawable.BitmapDrawable;
  26import android.graphics.drawable.Drawable;
  27import android.media.AudioManager;
  28import android.net.ConnectivityManager;
  29import android.net.Network;
  30import android.net.NetworkCapabilities;
  31import android.net.NetworkInfo;
  32import android.net.Uri;
  33import android.os.Binder;
  34import android.os.Build;
  35import android.os.Bundle;
  36import android.os.Environment;
  37import android.os.IBinder;
  38import android.os.Messenger;
  39import android.os.PowerManager;
  40import android.os.PowerManager.WakeLock;
  41import android.os.SystemClock;
  42import android.preference.PreferenceManager;
  43import android.provider.ContactsContract;
  44import android.provider.DocumentsContract;
  45import android.security.KeyChain;
  46import android.text.TextUtils;
  47import android.util.DisplayMetrics;
  48import android.util.Log;
  49import android.util.LruCache;
  50import android.util.Pair;
  51import androidx.annotation.BoolRes;
  52import androidx.annotation.IntegerRes;
  53import androidx.annotation.NonNull;
  54import androidx.annotation.Nullable;
  55import androidx.core.app.RemoteInput;
  56import androidx.core.content.ContextCompat;
  57
  58import com.cheogram.android.EmojiSearch;
  59import com.cheogram.android.WebxdcUpdate;
  60
  61import com.google.common.base.Objects;
  62import com.google.common.base.Optional;
  63import com.google.common.base.Strings;
  64import com.google.common.collect.Collections2;
  65import com.google.common.collect.ImmutableMap;
  66import com.google.common.collect.ImmutableSet;
  67import com.google.common.collect.Iterables;
  68import com.google.common.collect.Maps;
  69import com.google.common.collect.Multimap;
  70import com.google.common.io.Files;
  71
  72import com.kedia.ogparser.JsoupProxy;
  73import com.kedia.ogparser.OpenGraphCallback;
  74import com.kedia.ogparser.OpenGraphParser;
  75import com.kedia.ogparser.OpenGraphResult;
  76
  77import org.conscrypt.Conscrypt;
  78import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep;
  79import org.openintents.openpgp.IOpenPgpService2;
  80import org.openintents.openpgp.util.OpenPgpApi;
  81import org.openintents.openpgp.util.OpenPgpServiceConnection;
  82
  83import java.io.File;
  84import java.io.FileInputStream;
  85import java.io.IOException;
  86import java.net.URI;
  87import java.security.Security;
  88import java.security.cert.CertificateException;
  89import java.security.cert.X509Certificate;
  90import java.util.ArrayList;
  91import java.util.Arrays;
  92import java.util.Collection;
  93import java.util.Collections;
  94import java.util.HashMap;
  95import java.util.HashSet;
  96import java.util.Hashtable;
  97import java.util.Iterator;
  98import java.util.List;
  99import java.util.ListIterator;
 100import java.util.Map;
 101import java.util.Set;
 102import java.util.WeakHashMap;
 103import java.util.concurrent.CopyOnWriteArrayList;
 104import java.util.concurrent.CountDownLatch;
 105import java.util.concurrent.Executor;
 106import java.util.concurrent.Executors;
 107import java.util.concurrent.Semaphore;
 108import java.util.concurrent.RejectedExecutionException;
 109import java.util.concurrent.ScheduledExecutorService;
 110import java.util.concurrent.TimeUnit;
 111import java.util.concurrent.atomic.AtomicBoolean;
 112import java.util.concurrent.atomic.AtomicLong;
 113import java.util.concurrent.atomic.AtomicReference;
 114import java.util.function.Consumer;
 115
 116import io.ipfs.cid.Cid;
 117
 118import com.google.common.util.concurrent.FutureCallback;
 119import com.google.common.util.concurrent.Futures;
 120import com.google.common.util.concurrent.MoreExecutors;
 121import eu.siacs.conversations.AppSettings;
 122import eu.siacs.conversations.Config;
 123import eu.siacs.conversations.R;
 124import eu.siacs.conversations.android.JabberIdContact;
 125import eu.siacs.conversations.crypto.OmemoSetting;
 126import eu.siacs.conversations.crypto.PgpDecryptionService;
 127import eu.siacs.conversations.crypto.PgpEngine;
 128import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 129import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 130import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
 131import eu.siacs.conversations.entities.Account;
 132import eu.siacs.conversations.entities.Blockable;
 133import eu.siacs.conversations.entities.Bookmark;
 134import eu.siacs.conversations.entities.Contact;
 135import eu.siacs.conversations.entities.Conversation;
 136import eu.siacs.conversations.entities.Conversational;
 137import eu.siacs.conversations.entities.DownloadableFile;
 138import eu.siacs.conversations.entities.Message;
 139import eu.siacs.conversations.entities.MucOptions;
 140import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
 141import eu.siacs.conversations.entities.Presence;
 142import eu.siacs.conversations.entities.PresenceTemplate;
 143import eu.siacs.conversations.entities.Reaction;
 144import eu.siacs.conversations.entities.Roster;
 145import eu.siacs.conversations.entities.ServiceDiscoveryResult;
 146import eu.siacs.conversations.generator.AbstractGenerator;
 147import eu.siacs.conversations.generator.IqGenerator;
 148import eu.siacs.conversations.generator.MessageGenerator;
 149import eu.siacs.conversations.generator.PresenceGenerator;
 150import eu.siacs.conversations.http.HttpConnectionManager;
 151import eu.siacs.conversations.http.ServiceOutageStatus;
 152import eu.siacs.conversations.parser.AbstractParser;
 153import eu.siacs.conversations.parser.IqParser;
 154import eu.siacs.conversations.persistance.DatabaseBackend;
 155import eu.siacs.conversations.persistance.FileBackend;
 156import eu.siacs.conversations.persistance.UnifiedPushDatabase;
 157import eu.siacs.conversations.receiver.SystemEventReceiver;
 158import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity;
 159import eu.siacs.conversations.ui.ConversationsActivity;
 160import eu.siacs.conversations.ui.RtpSessionActivity;
 161import eu.siacs.conversations.ui.UiCallback;
 162import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
 163import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
 164import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
 165import eu.siacs.conversations.ui.util.QuoteHelper;
 166import eu.siacs.conversations.utils.AccountUtils;
 167import eu.siacs.conversations.utils.Compatibility;
 168import eu.siacs.conversations.utils.ConversationsFileObserver;
 169import eu.siacs.conversations.utils.CryptoHelper;
 170import eu.siacs.conversations.utils.Emoticons;
 171import eu.siacs.conversations.utils.EasyOnboardingInvite;
 172import eu.siacs.conversations.utils.ExceptionHelper;
 173import eu.siacs.conversations.utils.FileUtils;
 174import eu.siacs.conversations.utils.MessageUtils;
 175import eu.siacs.conversations.utils.Emoticons;
 176import eu.siacs.conversations.utils.MimeUtils;
 177import eu.siacs.conversations.utils.PhoneHelper;
 178import eu.siacs.conversations.utils.QuickLoader;
 179import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
 180import eu.siacs.conversations.utils.ReplacingTaskManager;
 181import eu.siacs.conversations.utils.Resolver;
 182import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
 183import eu.siacs.conversations.utils.StringUtils;
 184import eu.siacs.conversations.utils.TorServiceUtils;
 185import eu.siacs.conversations.utils.ThemeHelper;
 186import eu.siacs.conversations.utils.WakeLockHelper;
 187import eu.siacs.conversations.utils.XmppUri;
 188import eu.siacs.conversations.xml.Element;
 189import eu.siacs.conversations.xml.LocalizedContent;
 190import eu.siacs.conversations.xml.Namespace;
 191import eu.siacs.conversations.xmpp.Jid;
 192import eu.siacs.conversations.xmpp.OnContactStatusChanged;
 193import eu.siacs.conversations.xmpp.OnGatewayResult;
 194import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 195import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
 196import eu.siacs.conversations.xmpp.OnStatusChanged;
 197import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 198import eu.siacs.conversations.xmpp.XmppConnection;
 199import eu.siacs.conversations.xmpp.chatstate.ChatState;
 200import eu.siacs.conversations.xmpp.forms.Data;
 201import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
 202import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
 203import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 204import eu.siacs.conversations.xmpp.jingle.Media;
 205import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
 206import eu.siacs.conversations.xmpp.mam.MamReference;
 207import eu.siacs.conversations.xmpp.pep.Avatar;
 208import eu.siacs.conversations.xmpp.pep.PublishOptions;
 209import im.conversations.android.xmpp.model.avatar.Metadata;
 210import im.conversations.android.xmpp.model.bookmark.Storage;
 211import im.conversations.android.xmpp.model.mds.Displayed;
 212import im.conversations.android.xmpp.model.pubsub.PubSub;
 213import im.conversations.android.xmpp.model.stanza.Iq;
 214import im.conversations.android.xmpp.model.storage.PrivateStorage;
 215import java.io.File;
 216import java.security.Security;
 217import java.security.cert.CertificateException;
 218import java.security.cert.X509Certificate;
 219import java.util.ArrayList;
 220import java.util.Arrays;
 221import java.util.Collection;
 222import java.util.Collections;
 223import java.util.HashSet;
 224import java.util.Iterator;
 225import java.util.List;
 226import java.util.ListIterator;
 227import java.util.Map;
 228import java.util.Set;
 229import java.util.WeakHashMap;
 230import java.util.concurrent.CopyOnWriteArrayList;
 231import java.util.concurrent.CountDownLatch;
 232import java.util.concurrent.Executor;
 233import java.util.concurrent.Executors;
 234import java.util.concurrent.RejectedExecutionException;
 235import java.util.concurrent.ScheduledExecutorService;
 236import java.util.concurrent.TimeUnit;
 237import java.util.concurrent.atomic.AtomicBoolean;
 238import java.util.concurrent.atomic.AtomicLong;
 239import java.util.concurrent.atomic.AtomicReference;
 240import java.util.function.Consumer;
 241import me.leolin.shortcutbadger.ShortcutBadger;
 242import okhttp3.HttpUrl;
 243import org.conscrypt.Conscrypt;
 244import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep;
 245import org.openintents.openpgp.IOpenPgpService2;
 246import org.openintents.openpgp.util.OpenPgpApi;
 247import org.openintents.openpgp.util.OpenPgpServiceConnection;
 248
 249import okhttp3.HttpUrl;
 250import okhttp3.OkHttpClient;
 251
 252public class XmppConnectionService extends Service {
 253
 254    public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations";
 255    public static final String ACTION_MARK_AS_READ = "mark_as_read";
 256    public static final String ACTION_SNOOZE = "snooze";
 257    public static final String ACTION_CLEAR_MESSAGE_NOTIFICATION = "clear_message_notification";
 258    public static final String ACTION_CLEAR_MISSED_CALL_NOTIFICATION =
 259            "clear_missed_call_notification";
 260    public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error";
 261    public static final String ACTION_TRY_AGAIN = "try_again";
 262
 263    public static final String ACTION_TEMPORARILY_DISABLE = "temporarily_disable";
 264    public static final String ACTION_PING = "ping";
 265    public static final String ACTION_IDLE_PING = "idle_ping";
 266    public static final String ACTION_INTERNAL_PING = "internal_ping";
 267    public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh";
 268    public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received";
 269    public static final String ACTION_DISMISS_CALL = "dismiss_call";
 270    public static final String ACTION_END_CALL = "end_call";
 271    public static final String ACTION_STARTING_CALL = "starting_call";
 272    public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
 273    public static final String ACTION_CALL_INTEGRATION_SERVICE_STARTED =
 274            "call_integration_service_started";
 275    private static final String ACTION_POST_CONNECTIVITY_CHANGE =
 276            "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
 277    public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS =
 278            "eu.siacs.conversations.UNIFIED_PUSH_RENEW";
 279    public static final String ACTION_QUICK_LOG = "eu.siacs.conversations.QUICK_LOG";
 280
 281    private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
 282
 283    public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
 284    private static final Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor();
 285    public static final Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor();
 286
 287    private final ScheduledExecutorService internalPingExecutor =
 288            Executors.newSingleThreadScheduledExecutor();
 289    private static final SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR =
 290            new SerialSingleThreadExecutor("VideoCompression");
 291    private final SerialSingleThreadExecutor mDatabaseWriterExecutor =
 292            new SerialSingleThreadExecutor("DatabaseWriter");
 293    private final SerialSingleThreadExecutor mDatabaseReaderExecutor =
 294            new SerialSingleThreadExecutor("DatabaseReader");
 295    private final SerialSingleThreadExecutor mNotificationExecutor =
 296            new SerialSingleThreadExecutor("NotificationExecutor");
 297    private final ReplacingTaskManager mRosterSyncTaskManager = new ReplacingTaskManager();
 298    private final IBinder mBinder = new XmppConnectionBinder();
 299    private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
 300    private final IqGenerator mIqGenerator = new IqGenerator(this);
 301    private final Set<String> mInProgressAvatarFetches = new HashSet<>();
 302    private final Set<String> mOmittedPepAvatarFetches = new HashSet<>();
 303    private final HashSet<Jid> mLowPingTimeoutMode = new HashSet<>();
 304    private final Consumer<Iq> mDefaultIqHandler =
 305            (packet) -> {
 306                if (packet.getType() != Iq.Type.RESULT) {
 307                    final var error = packet.getError();
 308                    String text = error != null ? error.findChildContent("text") : null;
 309                    if (text != null) {
 310                        Log.d(Config.LOGTAG, "received iq error: " + text);
 311                    }
 312                }
 313            };
 314    public DatabaseBackend databaseBackend;
 315    private Multimap<String, String> mutedMucUsers;
 316    private final ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger");
 317    private final ReplacingSerialSingleThreadExecutor mStickerScanExecutor = new ReplacingSerialSingleThreadExecutor("StickerScan");
 318    private long mLastActivity = 0;
 319    private long mLastMucPing = 0;
 320    private Map<String, Message> mScheduledMessages = new HashMap<>();
 321    private long mLastStickerRescan = 0;
 322    private final AppSettings appSettings = new AppSettings(this);
 323    private final FileBackend fileBackend = new FileBackend(this);
 324    private MemorizingTrustManager mMemorizingTrustManager;
 325    private final NotificationService mNotificationService = new NotificationService(this);
 326    private final UnifiedPushBroker unifiedPushBroker = new UnifiedPushBroker(this);
 327    private final ChannelDiscoveryService mChannelDiscoveryService =
 328            new ChannelDiscoveryService(this);
 329    private final ShortcutService mShortcutService = new ShortcutService(this);
 330    private final AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false);
 331    private final AtomicBoolean mOngoingVideoTranscoding = new AtomicBoolean(false);
 332    private final AtomicBoolean mForceDuringOnCreate = new AtomicBoolean(false);
 333    private final AtomicReference<OngoingCall> ongoingCall = new AtomicReference<>();
 334    private final MessageGenerator mMessageGenerator = new MessageGenerator(this);
 335    public OnContactStatusChanged onContactStatusChanged =
 336            (contact, online) -> {
 337                final var conversation = find(contact);
 338                if (conversation == null) {
 339                    return;
 340                }
 341                if (online) {
 342                    if (contact.getPresences().size() == 1) {
 343                        sendUnsentMessages(conversation);
 344                    }
 345                }
 346            };
 347    private final PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
 348    private List<Account> accounts;
 349    private final JingleConnectionManager mJingleConnectionManager =
 350            new JingleConnectionManager(this);
 351    private final HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(this);
 352    private final AvatarService mAvatarService = new AvatarService(this);
 353    private final MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
 354    private final PushManagementService mPushManagementService = new PushManagementService(this);
 355    private final QuickConversationsService mQuickConversationsService =
 356            new QuickConversationsService(this);
 357    private final ConversationsFileObserver fileObserver =
 358            new ConversationsFileObserver(
 359                    Environment.getExternalStorageDirectory().getAbsolutePath()) {
 360                @Override
 361                public void onEvent(final int event, final File file) {
 362                    markFileDeleted(file);
 363                }
 364            };
 365    private final OnMessageAcknowledged mOnMessageAcknowledgedListener =
 366            new OnMessageAcknowledged() {
 367
 368                @Override
 369                public boolean onMessageAcknowledged(
 370                        final Account account, final Jid to, final String id) {
 371                    if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
 372                        final String sessionId =
 373                                id.substring(
 374                                        JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX
 375                                                .length());
 376                        mJingleConnectionManager.updateProposedSessionDiscovered(
 377                                account,
 378                                to,
 379                                sessionId,
 380                                JingleConnectionManager.DeviceDiscoveryState
 381                                        .SEARCHING_ACKNOWLEDGED);
 382                    }
 383
 384                    final Jid bare = to.asBareJid();
 385
 386                    for (final Conversation conversation : getConversations()) {
 387                        if (conversation.getAccount() == account
 388                                && conversation.getJid().asBareJid().equals(bare)) {
 389                            final Message message = conversation.findUnsentMessageWithUuid(id);
 390                            if (message != null) {
 391                                message.setStatus(Message.STATUS_SEND);
 392                                message.setErrorMessage(null);
 393                                databaseBackend.updateMessage(message, false);
 394                                return true;
 395                            }
 396                        }
 397                    }
 398                    return false;
 399                }
 400            };
 401
 402    private final AtomicBoolean diallerIntegrationActive = new AtomicBoolean(false);
 403
 404    public void setDiallerIntegrationActive(boolean active) {
 405      diallerIntegrationActive.set(active);
 406    }
 407
 408    private boolean destroyed = false;
 409
 410    private int unreadCount = -1;
 411
 412    // Ui callback listeners
 413    private final Set<OnConversationUpdate> mOnConversationUpdates =
 414            Collections.newSetFromMap(new WeakHashMap<OnConversationUpdate, Boolean>());
 415    private final Set<OnShowErrorToast> mOnShowErrorToasts =
 416            Collections.newSetFromMap(new WeakHashMap<OnShowErrorToast, Boolean>());
 417    private final Set<OnAccountUpdate> mOnAccountUpdates =
 418            Collections.newSetFromMap(new WeakHashMap<OnAccountUpdate, Boolean>());
 419    private final Set<OnCaptchaRequested> mOnCaptchaRequested =
 420            Collections.newSetFromMap(new WeakHashMap<OnCaptchaRequested, Boolean>());
 421    private final Set<OnRosterUpdate> mOnRosterUpdates =
 422            Collections.newSetFromMap(new WeakHashMap<OnRosterUpdate, Boolean>());
 423    private final Set<OnUpdateBlocklist> mOnUpdateBlocklist =
 424            Collections.newSetFromMap(new WeakHashMap<OnUpdateBlocklist, Boolean>());
 425    private final Set<OnMucRosterUpdate> mOnMucRosterUpdate =
 426            Collections.newSetFromMap(new WeakHashMap<OnMucRosterUpdate, Boolean>());
 427    private final Set<OnKeyStatusUpdated> mOnKeyStatusUpdated =
 428            Collections.newSetFromMap(new WeakHashMap<OnKeyStatusUpdated, Boolean>());
 429    private final Set<OnJingleRtpConnectionUpdate> onJingleRtpConnectionUpdate =
 430            Collections.newSetFromMap(new WeakHashMap<OnJingleRtpConnectionUpdate, Boolean>());
 431
 432    private final Object LISTENER_LOCK = new Object();
 433
 434    public final Set<String> FILENAMES_TO_IGNORE_DELETION = new HashSet<>();
 435
 436    private final AtomicLong mLastExpiryRun = new AtomicLong(0);
 437    private final LruCache<Pair<String, String>, ServiceDiscoveryResult> discoCache =
 438            new LruCache<>(20);
 439    private final OnStatusChanged statusListener =
 440            new OnStatusChanged() {
 441
 442                @Override
 443                public void onStatusChanged(final Account account) {
 444                    final var status = account.getStatus();
 445                    if (ServiceOutageStatus.isPossibleOutage(status)) {
 446                        fetchServiceOutageStatus(account);
 447                    }
 448                    XmppConnection connection = account.getXmppConnection();
 449                    updateAccountUi();
 450
 451                    if (account.getStatus() == Account.State.ONLINE
 452                            || account.getStatus().isError()) {
 453                        mQuickConversationsService.signalAccountStateChange();
 454                    }
 455
 456                    if (account.getStatus() == Account.State.ONLINE) {
 457                        synchronized (mLowPingTimeoutMode) {
 458                            if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
 459                                Log.d(
 460                                        Config.LOGTAG,
 461                                        account.getJid().asBareJid()
 462                                                + ": leaving low ping timeout mode");
 463                            }
 464                        }
 465                        if (account.setShowErrorNotification(true)) {
 466                            databaseBackend.updateAccount(account);
 467                        }
 468                        mMessageArchiveService.executePendingQueries(account);
 469                        if (connection != null && connection.getFeatures().csi()) {
 470                            if (checkListeners()) {
 471                                Log.d(
 472                                        Config.LOGTAG,
 473                                        account.getJid().asBareJid() + " sending csi//inactive");
 474                                connection.sendInactive();
 475                            } else {
 476                                Log.d(
 477                                        Config.LOGTAG,
 478                                        account.getJid().asBareJid() + " sending csi//active");
 479                                connection.sendActive();
 480                            }
 481                        }
 482                        List<Conversation> conversations = getConversations();
 483                        for (Conversation conversation : conversations) {
 484                            final boolean inProgressJoin;
 485                            synchronized (account.inProgressConferenceJoins) {
 486                                inProgressJoin =
 487                                        account.inProgressConferenceJoins.contains(conversation);
 488                            }
 489                            final boolean pendingJoin;
 490                            synchronized (account.pendingConferenceJoins) {
 491                                pendingJoin = account.pendingConferenceJoins.contains(conversation);
 492                            }
 493                            if (conversation.getAccount() == account
 494                                    && !pendingJoin
 495                                    && !inProgressJoin) {
 496                                sendUnsentMessages(conversation);
 497                            }
 498                        }
 499                        final List<Conversation> pendingLeaves;
 500                        synchronized (account.pendingConferenceLeaves) {
 501                            pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves);
 502                            account.pendingConferenceLeaves.clear();
 503                        }
 504                        for (Conversation conversation : pendingLeaves) {
 505                            leaveMuc(conversation);
 506                        }
 507                        final List<Conversation> pendingJoins;
 508                        synchronized (account.pendingConferenceJoins) {
 509                            pendingJoins = new ArrayList<>(account.pendingConferenceJoins);
 510                            account.pendingConferenceJoins.clear();
 511                        }
 512                        for (Conversation conversation : pendingJoins) {
 513                            joinMuc(conversation);
 514                        }
 515                        scheduleWakeUpCall(
 516                                Config.PING_MAX_INTERVAL * 1000L, account.getUuid().hashCode());
 517                    } else if (account.getStatus() == Account.State.OFFLINE
 518                            || account.getStatus() == Account.State.DISABLED
 519                            || account.getStatus() == Account.State.LOGGED_OUT) {
 520                        resetSendingToWaiting(account);
 521                        if (account.isConnectionEnabled() && isInLowPingTimeoutMode(account)) {
 522                            Log.d(
 523                                    Config.LOGTAG,
 524                                    account.getJid().asBareJid()
 525                                            + ": went into offline state during low ping mode."
 526                                            + " reconnecting now");
 527                            reconnectAccount(account, true, false);
 528                        } else {
 529                            final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2;
 530                            scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
 531                        }
 532                    } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
 533                        databaseBackend.updateAccount(account);
 534                        reconnectAccount(account, true, false);
 535                    } else if (account.getStatus() != Account.State.CONNECTING
 536                            && account.getStatus() != Account.State.NO_INTERNET) {
 537                        resetSendingToWaiting(account);
 538                        if (connection != null && account.getStatus().isAttemptReconnect()) {
 539                            final boolean aggressive =
 540                                    account.getStatus() == Account.State.SEE_OTHER_HOST
 541                                            || hasJingleRtpConnection(account);
 542                            final int next = connection.getTimeToNextAttempt(aggressive);
 543                            final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account);
 544                            if (next <= 0) {
 545                                Log.d(
 546                                        Config.LOGTAG,
 547                                        account.getJid().asBareJid()
 548                                                + ": error connecting account. reconnecting now."
 549                                                + " lowPingTimeout="
 550                                                + lowPingTimeoutMode);
 551                                reconnectAccount(account, true, false);
 552                            } else {
 553                                final int attempt = connection.getAttempt() + 1;
 554                                Log.d(
 555                                        Config.LOGTAG,
 556                                        account.getJid().asBareJid()
 557                                                + ": error connecting account. try again in "
 558                                                + next
 559                                                + "s for the "
 560                                                + attempt
 561                                                + " time. lowPingTimeout="
 562                                                + lowPingTimeoutMode
 563                                                + ", aggressive="
 564                                                + aggressive);
 565                                scheduleWakeUpCall(next, account.getUuid().hashCode());
 566                                if (aggressive) {
 567                                    internalPingExecutor.schedule(
 568                                            XmppConnectionService.this
 569                                                    ::manageAccountConnectionStatesInternal,
 570                                            (next * 1000L) + 50,
 571                                            TimeUnit.MILLISECONDS);
 572                                }
 573                            }
 574                        }
 575                    }
 576                    getNotificationService().updateErrorNotification();
 577                }
 578            };
 579
 580    private OpenPgpServiceConnection pgpServiceConnection;
 581    private PgpEngine mPgpEngine = null;
 582    private WakeLock wakeLock;
 583    private LruCache<String, Drawable> mDrawableCache;
 584    private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
 585    private final BroadcastReceiver mInternalRestrictedEventReceiver =
 586            new RestrictedEventReceiver(Arrays.asList(TorServiceUtils.ACTION_STATUS));
 587    private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
 588    private EmojiSearch emojiSearch = null;
 589
 590    private static String generateFetchKey(Account account, final Avatar avatar) {
 591        return account.getJid().asBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum;
 592    }
 593
 594    private boolean isInLowPingTimeoutMode(Account account) {
 595        synchronized (mLowPingTimeoutMode) {
 596            return mLowPingTimeoutMode.contains(account.getJid().asBareJid());
 597        }
 598    }
 599
 600    public void startOngoingVideoTranscodingForegroundNotification() {
 601        mOngoingVideoTranscoding.set(true);
 602        toggleForegroundService();
 603    }
 604
 605    public void stopOngoingVideoTranscodingForegroundNotification() {
 606        mOngoingVideoTranscoding.set(false);
 607        toggleForegroundService();
 608    }
 609
 610    public boolean areMessagesInitialized() {
 611        return this.restoredFromDatabaseLatch.getCount() == 0;
 612    }
 613
 614    public PgpEngine getPgpEngine() {
 615        if (!Config.supportOpenPgp()) {
 616            return null;
 617        } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
 618            if (this.mPgpEngine == null) {
 619                this.mPgpEngine =
 620                        new PgpEngine(
 621                                new OpenPgpApi(
 622                                        getApplicationContext(), pgpServiceConnection.getService()),
 623                                this);
 624            }
 625            return mPgpEngine;
 626        } else {
 627            return null;
 628        }
 629    }
 630
 631    public OpenPgpApi getOpenPgpApi() {
 632        if (!Config.supportOpenPgp()) {
 633            return null;
 634        } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
 635            return new OpenPgpApi(this, pgpServiceConnection.getService());
 636        } else {
 637            return null;
 638        }
 639    }
 640
 641    public AppSettings getAppSettings() {
 642        return this.appSettings;
 643    }
 644
 645    public FileBackend getFileBackend() {
 646        return this.fileBackend;
 647    }
 648
 649    public DownloadableFile getFileForCid(Cid cid) {
 650        return this.databaseBackend.getFileForCid(cid);
 651    }
 652
 653    public String getUrlForCid(Cid cid) {
 654        return this.databaseBackend.getUrlForCid(cid);
 655    }
 656
 657    public void saveCid(Cid cid, File file) throws BlockedMediaException {
 658        saveCid(cid, file, null);
 659    }
 660
 661    public void saveCid(Cid cid, File file, String url) throws BlockedMediaException {
 662        if (this.databaseBackend.isBlockedMedia(cid)) {
 663            throw new BlockedMediaException();
 664        }
 665        this.databaseBackend.saveCid(cid, file, url);
 666    }
 667
 668    public boolean muteMucUser(MucOptions.User user) {
 669        boolean muted = databaseBackend.muteMucUser(user);
 670        if (!muted) return false;
 671        mutedMucUsers.put(user.getMuc().toString(), user.getOccupantId());
 672        return true;
 673    }
 674
 675    public boolean unmuteMucUser(MucOptions.User user) {
 676        boolean unmuted = databaseBackend.unmuteMucUser(user);
 677        if (!unmuted) return false;
 678        mutedMucUsers.remove(user.getMuc().toString(), user.getOccupantId());
 679        return true;
 680    }
 681
 682    public boolean isMucUserMuted(MucOptions.User user) {
 683        return mutedMucUsers.containsEntry("" + user.getMuc(), user.getOccupantId());
 684    }
 685
 686    public void blockMedia(File f) {
 687        try {
 688            Cid[] cids = getFileBackend().calculateCids(new FileInputStream(f));
 689            for (Cid cid : cids) {
 690                blockMedia(cid);
 691            }
 692        } catch (final IOException e) { }
 693    }
 694
 695    public void blockMedia(Cid cid) {
 696        this.databaseBackend.blockMedia(cid);
 697    }
 698
 699    public void clearBlockedMedia() {
 700        this.databaseBackend.clearBlockedMedia();
 701    }
 702
 703    public Message getMessage(Conversation conversation, String uuid) {
 704        return this.databaseBackend.getMessage(conversation, uuid);
 705    }
 706
 707    public Map<String, Message> getMessageFuzzyIds(Conversation conversation, Collection<String> ids) {
 708        return this.databaseBackend.getMessageFuzzyIds(conversation, ids);
 709    }
 710
 711    public void insertWebxdcUpdate(final WebxdcUpdate update) {
 712        this.databaseBackend.insertWebxdcUpdate(update);
 713    }
 714
 715    public WebxdcUpdate findLastWebxdcUpdate(Message message) {
 716        return this.databaseBackend.findLastWebxdcUpdate(message);
 717    }
 718
 719    public List<WebxdcUpdate> findWebxdcUpdates(Message message, long serial) {
 720        return this.databaseBackend.findWebxdcUpdates(message, serial);
 721    }
 722
 723    public AvatarService getAvatarService() {
 724        return this.mAvatarService;
 725    }
 726
 727    public void attachLocationToConversation(
 728            final Conversation conversation, final Uri uri, final String subject, final UiCallback<Message> callback) {
 729        int encryption = conversation.getNextEncryption();
 730        if (encryption == Message.ENCRYPTION_PGP) {
 731            encryption = Message.ENCRYPTION_DECRYPTED;
 732        }
 733        Message message = new Message(conversation, uri.toString(), encryption);
 734        if (subject != null && subject.length() > 0) message.setSubject(subject);
 735        message.setThread(conversation.getThread());
 736        Message.configurePrivateMessage(message);
 737        if (encryption == Message.ENCRYPTION_DECRYPTED) {
 738            getPgpEngine().encrypt(message, callback);
 739        } else {
 740            sendMessage(message);
 741            callback.success(message);
 742        }
 743    }
 744
 745    public void attachFileToConversation(
 746            final Conversation conversation,
 747            final Uri uri,
 748            final String type,
 749            final String subject,
 750            final UiCallback<Message> callback) {
 751        final Message message;
 752        if (conversation.getReplyTo() == null) {
 753            message = new Message(conversation, "", conversation.getNextEncryption());
 754        } else {
 755            message = conversation.getReplyTo().reply();
 756            message.setEncryption(conversation.getNextEncryption());
 757        }
 758        if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 759            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 760        }
 761        if (subject != null && subject.length() > 0) message.setSubject(subject);
 762        message.setThread(conversation.getThread());
 763        if (!Message.configurePrivateFileMessage(message)) {
 764            message.setCounterpart(conversation.getNextCounterpart());
 765            message.setType(Message.TYPE_FILE);
 766        }
 767        Log.d(Config.LOGTAG, "attachFile: type=" + message.getType());
 768        Log.d(Config.LOGTAG, "counterpart=" + message.getCounterpart());
 769        final AttachFileToConversationRunnable runnable =
 770                new AttachFileToConversationRunnable(this, uri, type, message, callback);
 771        if (runnable.isVideoMessage()) {
 772            VIDEO_COMPRESSION_EXECUTOR.execute(runnable);
 773        } else {
 774            FILE_ATTACHMENT_EXECUTOR.execute(runnable);
 775        }
 776    }
 777
 778    public void attachImageToConversation(
 779            final Conversation conversation,
 780            final Uri uri,
 781            final String type,
 782            final String subject,
 783            final UiCallback<Message> callback) {
 784        final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type);
 785        final String compressPictures = getCompressPicturesPreference();
 786
 787        if ("never".equals(compressPictures)
 788                || ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))
 789                || (mimeType != null && mimeType.endsWith("/gif"))
 790                || getFileBackend().unusualBounds(uri) || "data".equals(uri.getScheme())) {
 791            Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": not compressing picture. sending as file");
 792            attachFileToConversation(conversation, uri, mimeType, subject, callback);
 793            return;
 794        }
 795        final Message message;
 796
 797        if (conversation.getReplyTo() == null) {
 798            message = new Message(conversation, "", conversation.getNextEncryption());
 799        } else {
 800            message = conversation.getReplyTo().reply();
 801            message.setEncryption(conversation.getNextEncryption());
 802        }
 803        if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 804            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 805        }
 806        if (subject != null && subject.length() > 0) message.setSubject(subject);
 807        message.setThread(conversation.getThread());
 808        if (!Message.configurePrivateFileMessage(message)) {
 809            message.setCounterpart(conversation.getNextCounterpart());
 810            message.setType(Message.TYPE_IMAGE);
 811        }
 812        Log.d(Config.LOGTAG, "attachImage: type=" + message.getType());
 813        FILE_ATTACHMENT_EXECUTOR.execute(() -> {
 814            try {
 815                getFileBackend().copyImageToPrivateStorage(message, uri);
 816            } catch (FileBackend.ImageCompressionException e) {
 817                Log.d(Config.LOGTAG, "unable to compress image. fall back to file transfer", e);
 818                attachFileToConversation(conversation, uri, mimeType, subject, callback);
 819                return;
 820            } catch (final FileBackend.FileCopyException e) {
 821                callback.error(e.getResId(), message);
 822                return;
 823            }
 824            if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 825                final PgpEngine pgpEngine = getPgpEngine();
 826                if (pgpEngine != null) {
 827                    pgpEngine.encrypt(message, callback);
 828                } else if (callback != null) {
 829                    callback.error(R.string.unable_to_connect_to_keychain, null);
 830                }
 831            } else {
 832                sendMessage(message, false, false, false, () -> callback.success(message));
 833            }
 834        });
 835    }
 836
 837    private File stickerDir() {
 838        SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
 839        final String dir = p.getString("sticker_directory", "Stickers");
 840        if (dir.startsWith("content://")) {
 841            Uri uri = Uri.parse(dir);
 842            uri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri));
 843            return new File(FileUtils.getPath(getBaseContext(), uri));
 844        } else {
 845            return new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/" + dir);
 846        }
 847    }
 848
 849    public void rescanStickers() {
 850        long msToRescan = (mLastStickerRescan + 600000L) - SystemClock.elapsedRealtime();
 851        if (msToRescan > 0) return;
 852        Log.d(Config.LOGTAG, "rescanStickers");
 853
 854        mLastStickerRescan = SystemClock.elapsedRealtime();
 855        mStickerScanExecutor.execute(() -> {
 856            Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
 857            try {
 858                for (File file : Files.fileTraverser().breadthFirst(stickerDir())) {
 859                    try {
 860                        if (file.isFile() && file.canRead()) {
 861                            DownloadableFile df = new DownloadableFile(file.getAbsolutePath());
 862                            Drawable icon = fileBackend.getThumbnail(df, getResources(), (int) (getResources().getDisplayMetrics().density * 288), false);
 863                            final String filename = Files.getNameWithoutExtension(df.getName());
 864                            Cid[] cids = fileBackend.calculateCids(new FileInputStream(df));
 865                            for (Cid cid : cids) {
 866                                saveCid(cid, file);
 867                            }
 868                            if (file.length() < 129000) {
 869                                emojiSearch.addEmoji(new EmojiSearch.CustomEmoji(filename, cids[0].toString(), icon, file.getParentFile().getName()));
 870                            }
 871                        }
 872                    } catch (final Exception e) {
 873                        Log.w(Config.LOGTAG, "rescanStickers: " + e);
 874                    }
 875                }
 876            } catch (final Exception e) {
 877                Log.w(Config.LOGTAG, "rescanStickers: " + e);
 878            }
 879        });
 880    }
 881
 882    protected void cleanupCache() {
 883        if (Build.VERSION.SDK_INT < 26) return; // Doesn't support file.toPath
 884        mStickerScanExecutor.execute(() -> {
 885            Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
 886            final var now = System.currentTimeMillis();
 887            try {
 888                for (File file : Files.fileTraverser().breadthFirst(getCacheDir())) {
 889                    if (file.isFile() && file.canRead() && file.canWrite()) {
 890                        final var attrs = java.nio.file.Files.readAttributes(file.toPath(), java.nio.file.attribute.BasicFileAttributes.class);
 891                        if ((now - attrs.lastAccessTime().toMillis()) > 1000L * 60 * 60 * 24 * 10) {
 892                            Log.d(Config.LOGTAG, "cleanupCache removing file not used recently: " + file);
 893                            file.delete();
 894                        }
 895                    }
 896                }
 897            } catch (final Exception e) {
 898                Log.w(Config.LOGTAG, "cleanupCache " + e);
 899            }
 900        });
 901    }
 902
 903    public EmojiSearch emojiSearch() {
 904        return emojiSearch;
 905    }
 906
 907    public Conversation find(Bookmark bookmark) {
 908        return find(bookmark.getAccount(), bookmark.getJid());
 909    }
 910
 911    public Conversation find(final Account account, final Jid jid) {
 912        return find(getConversations(), account, jid);
 913    }
 914
 915    public boolean isMuc(final Account account, final Jid jid) {
 916        final Conversation c = find(account, jid);
 917        return c != null && c.getMode() == Conversational.MODE_MULTI;
 918    }
 919
 920    public void search(
 921            final List<String> term,
 922            final String uuid,
 923            final OnSearchResultsAvailable onSearchResultsAvailable) {
 924        MessageSearchTask.search(this, term, uuid, onSearchResultsAvailable);
 925    }
 926
 927    @Override
 928    public int onStartCommand(final Intent intent, int flags, int startId) {
 929        final var nomedia = getBooleanPreference("nomedia", R.bool.default_nomedia);
 930        fileBackend.setupNomedia(nomedia);
 931        final String action = Strings.nullToEmpty(intent == null ? null : intent.getAction());
 932        final boolean needsForegroundService =
 933                intent != null
 934                        && intent.getBooleanExtra(
 935                                SystemEventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false);
 936        if (needsForegroundService) {
 937            Log.d(
 938                    Config.LOGTAG,
 939                    "toggle forced foreground service after receiving event (action="
 940                            + action
 941                            + ")");
 942            toggleForegroundService(true, action.equals(ACTION_STARTING_CALL));
 943        }
 944        final String uuid = intent == null ? null : intent.getStringExtra("uuid");
 945        switch (action) {
 946            case QuickConversationsService.SMS_RETRIEVED_ACTION:
 947                mQuickConversationsService.handleSmsReceived(intent);
 948                break;
 949            case ConnectivityManager.CONNECTIVITY_ACTION:
 950                if (hasInternetConnection()) {
 951                    if (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0) {
 952                        schedulePostConnectivityChange();
 953                    }
 954                    if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
 955                        resetAllAttemptCounts(true, false);
 956                    }
 957                    Resolver.clearCache();
 958                }
 959                break;
 960            case Intent.ACTION_SHUTDOWN:
 961                logoutAndSave(true);
 962                return START_NOT_STICKY;
 963            case ACTION_CLEAR_MESSAGE_NOTIFICATION:
 964                mNotificationExecutor.execute(
 965                        () -> {
 966                            try {
 967                                final Conversation c = findConversationByUuid(uuid);
 968                                if (c != null) {
 969                                    mNotificationService.clearMessages(c);
 970                                } else {
 971                                    mNotificationService.clearMessages();
 972                                }
 973                                restoredFromDatabaseLatch.await();
 974
 975                            } catch (InterruptedException e) {
 976                                Log.d(
 977                                        Config.LOGTAG,
 978                                        "unable to process clear message notification");
 979                            }
 980                        });
 981                break;
 982            case ACTION_CLEAR_MISSED_CALL_NOTIFICATION:
 983                mNotificationExecutor.execute(
 984                        () -> {
 985                            try {
 986                                final Conversation c = findConversationByUuid(uuid);
 987                                if (c != null) {
 988                                    mNotificationService.clearMissedCalls(c);
 989                                } else {
 990                                    mNotificationService.clearMissedCalls();
 991                                }
 992                                restoredFromDatabaseLatch.await();
 993
 994                            } catch (InterruptedException e) {
 995                                Log.d(
 996                                        Config.LOGTAG,
 997                                        "unable to process clear missed call notification");
 998                            }
 999                        });
1000                break;
1001            case ACTION_DISMISS_CALL:
1002                {
1003                    if (intent == null) {
1004                        break;
1005                    }
1006                    final String sessionId =
1007                            intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
1008                    Log.d(
1009                            Config.LOGTAG,
1010                            "received intent to dismiss call with session id " + sessionId);
1011                    mJingleConnectionManager.rejectRtpSession(sessionId);
1012                    break;
1013                }
1014            case TorServiceUtils.ACTION_STATUS:
1015                final String status =
1016                        intent == null ? null : intent.getStringExtra(TorServiceUtils.EXTRA_STATUS);
1017                // TODO port and host are in 'extras' - but this may not be a reliable source?
1018                if ("ON".equals(status)) {
1019                    handleOrbotStartedEvent();
1020                    return START_STICKY;
1021                }
1022                break;
1023            case ACTION_END_CALL:
1024                {
1025                    if (intent == null) {
1026                        break;
1027                    }
1028                    final String sessionId =
1029                            intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
1030                    Log.d(
1031                            Config.LOGTAG,
1032                            "received intent to end call with session id " + sessionId);
1033                    mJingleConnectionManager.endRtpSession(sessionId);
1034                }
1035                break;
1036            case ACTION_PROVISION_ACCOUNT:
1037                {
1038                    if (intent == null) {
1039                        break;
1040                    }
1041                    final String address = intent.getStringExtra("address");
1042                    final String password = intent.getStringExtra("password");
1043                    if (QuickConversationsService.isQuicksy()
1044                            || Strings.isNullOrEmpty(address)
1045                            || Strings.isNullOrEmpty(password)) {
1046                        break;
1047                    }
1048                    provisionAccount(address, password);
1049                    break;
1050                }
1051            case ACTION_DISMISS_ERROR_NOTIFICATIONS:
1052                dismissErrorNotifications();
1053                break;
1054            case ACTION_TRY_AGAIN:
1055                resetAllAttemptCounts(false, true);
1056                break;
1057            case ACTION_REPLY_TO_CONVERSATION:
1058                final Bundle remoteInput =
1059                        intent == null ? null : RemoteInput.getResultsFromIntent(intent);
1060                if (remoteInput == null) {
1061                    break;
1062                }
1063                final CharSequence body = remoteInput.getCharSequence("text_reply");
1064                final boolean dismissNotification =
1065                        intent.getBooleanExtra("dismiss_notification", false);
1066                final String lastMessageUuid = intent.getStringExtra("last_message_uuid");
1067                if (body == null || body.length() <= 0) {
1068                    break;
1069                }
1070                mNotificationExecutor.execute(
1071                        () -> {
1072                            try {
1073                                restoredFromDatabaseLatch.await();
1074                                final Conversation c = findConversationByUuid(uuid);
1075                                if (c != null) {
1076                                    directReply(
1077                                            c,
1078                                            body.toString(),
1079                                            lastMessageUuid,
1080                                            dismissNotification);
1081                                }
1082                            } catch (InterruptedException e) {
1083                                Log.d(Config.LOGTAG, "unable to process direct reply");
1084                            }
1085                        });
1086                break;
1087            case ACTION_MARK_AS_READ:
1088                mNotificationExecutor.execute(
1089                        () -> {
1090                            final Conversation c = findConversationByUuid(uuid);
1091                            if (c == null) {
1092                                Log.d(
1093                                        Config.LOGTAG,
1094                                        "received mark read intent for unknown conversation ("
1095                                                + uuid
1096                                                + ")");
1097                                return;
1098                            }
1099                            try {
1100                                restoredFromDatabaseLatch.await();
1101                                sendReadMarker(c, null);
1102                            } catch (InterruptedException e) {
1103                                Log.d(
1104                                        Config.LOGTAG,
1105                                        "unable to process notification read marker for"
1106                                                + " conversation "
1107                                                + c.getName());
1108                            }
1109                        });
1110                break;
1111            case ACTION_SNOOZE:
1112                mNotificationExecutor.execute(
1113                        () -> {
1114                            final Conversation c = findConversationByUuid(uuid);
1115                            if (c == null) {
1116                                Log.d(
1117                                        Config.LOGTAG,
1118                                        "received snooze intent for unknown conversation ("
1119                                                + uuid
1120                                                + ")");
1121                                return;
1122                            }
1123                            c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000);
1124                            mNotificationService.clearMessages(c);
1125                            updateConversation(c);
1126                        });
1127            case AudioManager.RINGER_MODE_CHANGED_ACTION:
1128            case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED:
1129                if (dndOnSilentMode()) {
1130                    refreshAllPresences();
1131                }
1132                break;
1133            case Intent.ACTION_SCREEN_ON:
1134                deactivateGracePeriod();
1135            case Intent.ACTION_USER_PRESENT:
1136            case Intent.ACTION_SCREEN_OFF:
1137                if (awayWhenScreenLocked()) {
1138                    refreshAllPresences();
1139                }
1140                break;
1141            case ACTION_FCM_TOKEN_REFRESH:
1142                refreshAllFcmTokens();
1143                break;
1144            case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS:
1145                if (intent == null) {
1146                    break;
1147                }
1148                final String instance = intent.getStringExtra("instance");
1149                final String application = intent.getStringExtra("application");
1150                final Messenger messenger = intent.getParcelableExtra("messenger");
1151                final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger;
1152                if (messenger != null && application != null && instance != null) {
1153                    pushTargetMessenger =
1154                            new UnifiedPushBroker.PushTargetMessenger(
1155                                    new UnifiedPushDatabase.PushTarget(application, instance),
1156                                    messenger);
1157                    Log.d(Config.LOGTAG, "found push target messenger");
1158                } else {
1159                    pushTargetMessenger = null;
1160                }
1161                final Optional<UnifiedPushBroker.Transport> transport =
1162                        renewUnifiedPushEndpoints(pushTargetMessenger);
1163                if (instance != null && transport.isPresent()) {
1164                    unifiedPushBroker.rebroadcastEndpoint(messenger, instance, transport.get());
1165                }
1166                break;
1167            case ACTION_IDLE_PING:
1168                scheduleNextIdlePing();
1169                break;
1170            case ACTION_FCM_MESSAGE_RECEIVED:
1171                Log.d(Config.LOGTAG, "push message arrived in service. account");
1172                break;
1173            case ACTION_QUICK_LOG:
1174                final String message = intent == null ? null : intent.getStringExtra("message");
1175                if (message != null && Config.QUICK_LOG) {
1176                    quickLog(message);
1177                }
1178                break;
1179            case Intent.ACTION_SEND:
1180                final Uri uri = intent == null ? null : intent.getData();
1181                if (uri != null) {
1182                    Log.d(Config.LOGTAG, "received uri permission for " + uri);
1183                }
1184                return START_STICKY;
1185            case ACTION_TEMPORARILY_DISABLE:
1186                toggleSoftDisabled(true);
1187                if (checkListeners()) {
1188                    stopSelf();
1189                }
1190                return START_NOT_STICKY;
1191        }
1192        sendScheduledMessages();
1193        final var extras =  intent == null ? null : intent.getExtras();
1194        try {
1195            internalPingExecutor.execute(() -> manageAccountConnectionStates(action, extras));
1196        } catch (final RejectedExecutionException e) {
1197            Log.e(Config.LOGTAG, "can not schedule connection states manager");
1198        }
1199        if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) {
1200            expireOldMessages();
1201        }
1202        return START_STICKY;
1203    }
1204
1205    private void quickLog(final String message) {
1206        if (Strings.isNullOrEmpty(message)) {
1207            return;
1208        }
1209        final Account account = AccountUtils.getFirstEnabled(this);
1210        if (account == null) {
1211            return;
1212        }
1213        final Conversation conversation =
1214                findOrCreateConversation(account, Config.BUG_REPORTS, false, true);
1215        final Message report = new Message(conversation, message, Message.ENCRYPTION_NONE);
1216        report.setStatus(Message.STATUS_RECEIVED);
1217        conversation.add(report);
1218        databaseBackend.createMessage(report);
1219        updateConversationUi();
1220    }
1221
1222    private void manageAccountConnectionStatesInternal() {
1223        manageAccountConnectionStates(ACTION_INTERNAL_PING, null);
1224    }
1225
1226    private synchronized void manageAccountConnectionStates(
1227            final String action, final Bundle extras) {
1228        final String pushedAccountHash = extras == null ? null : extras.getString("account");
1229        final boolean interactive = java.util.Objects.equals(ACTION_TRY_AGAIN, action);
1230        WakeLockHelper.acquire(wakeLock);
1231        boolean pingNow =
1232                ConnectivityManager.CONNECTIVITY_ACTION.equals(action)
1233                        || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0
1234                                && ACTION_POST_CONNECTIVITY_CHANGE.equals(action));
1235        final HashSet<Account> pingCandidates = new HashSet<>();
1236        final String androidId = pushedAccountHash == null ? null : PhoneHelper.getAndroidId(this);
1237        for (final Account account : accounts) {
1238            final boolean pushWasMeantForThisAccount =
1239                    androidId != null
1240                            && CryptoHelper.getAccountFingerprint(account, androidId)
1241                                    .equals(pushedAccountHash);
1242            pingNow |=
1243                    processAccountState(
1244                            account,
1245                            interactive,
1246                            "ui".equals(action),
1247                            pushWasMeantForThisAccount,
1248                            pingCandidates);
1249        }
1250        if (pingNow) {
1251            for (final Account account : pingCandidates) {
1252                final var connection = account.getXmppConnection();
1253                final boolean lowTimeout = isInLowPingTimeoutMode(account);
1254                final var delta =
1255                        (SystemClock.elapsedRealtime() - connection.getLastPacketReceived())
1256                                / 1000L;
1257                connection.sendPing();
1258                Log.d(
1259                        Config.LOGTAG,
1260                        String.format(
1261                                "%s: send ping (action=%s,lowTimeout=%s,interval=%s)",
1262                                account.getJid().asBareJid(), action, lowTimeout, delta));
1263                scheduleWakeUpCall(
1264                        lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT,
1265                        account.getUuid().hashCode());
1266            }
1267        }
1268        long msToMucPing = (mLastMucPing + (Config.PING_MAX_INTERVAL * 2000L)) - SystemClock.elapsedRealtime();
1269        if (pingNow || ("ui".equals(action) && msToMucPing <= 0) || msToMucPing < -300000) {
1270            Log.d(Config.LOGTAG, "ping MUCs");
1271            mLastMucPing = SystemClock.elapsedRealtime();
1272            for (Conversation c : getConversations()) {
1273                if (c.getMode() == Conversation.MODE_MULTI && (c.getMucOptions().online() || c.getMucOptions().getError() == MucOptions.Error.SHUTDOWN)) {
1274                    mucSelfPingAndRejoin(c);
1275                }
1276            }
1277        }
1278        WakeLockHelper.release(wakeLock);
1279    }
1280
1281    private void sendScheduledMessages() {
1282        Log.d(Config.LOGTAG, "looking for and sending scheduled messages");
1283
1284        for (final var message : new ArrayList<>(mScheduledMessages.values())) {
1285            if (message.getTimeSent() > System.currentTimeMillis()) continue;
1286
1287            final var conversation = message.getConversation();
1288            final var account = conversation.getAccount();
1289            final boolean inProgressJoin;
1290            synchronized (account.inProgressConferenceJoins) {
1291                inProgressJoin = account.inProgressConferenceJoins.contains(conversation);
1292            }
1293            final boolean pendingJoin;
1294            synchronized (account.pendingConferenceJoins) {
1295                pendingJoin = account.pendingConferenceJoins.contains(conversation);
1296            }
1297            if (conversation.getAccount() == account
1298                    && !pendingJoin
1299                    && !inProgressJoin) {
1300                resendMessage(message, false);
1301            }
1302        }
1303    }
1304
1305    private void handleOrbotStartedEvent() {
1306        for (final Account account : accounts) {
1307            if (account.getStatus() == Account.State.TOR_NOT_AVAILABLE) {
1308                reconnectAccount(account, true, false);
1309            }
1310        }
1311    }
1312
1313    private boolean processAccountState(
1314            final Account account,
1315            final boolean interactive,
1316            final boolean isUiAction,
1317            final boolean isAccountPushed,
1318            final HashSet<Account> pingCandidates) {
1319        if (!account.getStatus().isAttemptReconnect()) {
1320            return false;
1321        }
1322        final var requestCode = account.getUuid().hashCode();
1323        if (!hasInternetConnection()) {
1324            account.setStatus(Account.State.NO_INTERNET);
1325            statusListener.onStatusChanged(account);
1326        } else {
1327            if (account.getStatus() == Account.State.NO_INTERNET) {
1328                account.setStatus(Account.State.OFFLINE);
1329                statusListener.onStatusChanged(account);
1330            }
1331            if (account.getStatus() == Account.State.ONLINE) {
1332                synchronized (mLowPingTimeoutMode) {
1333                    long lastReceived = account.getXmppConnection().getLastPacketReceived();
1334                    long lastSent = account.getXmppConnection().getLastPingSent();
1335                    long pingInterval =
1336                            isUiAction
1337                                    ? Config.PING_MIN_INTERVAL * 1000
1338                                    : Config.PING_MAX_INTERVAL * 1000;
1339                    long msToNextPing =
1340                            (Math.max(lastReceived, lastSent) + pingInterval)
1341                                    - SystemClock.elapsedRealtime();
1342                    int pingTimeout =
1343                            mLowPingTimeoutMode.contains(account.getJid().asBareJid())
1344                                    ? Config.LOW_PING_TIMEOUT * 1000
1345                                    : Config.PING_TIMEOUT * 1000;
1346                    long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
1347                    if (lastSent > lastReceived) {
1348                        if (pingTimeoutIn < 0) {
1349                            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping timeout");
1350                            this.reconnectAccount(account, true, interactive);
1351                        } else {
1352                            this.scheduleWakeUpCall(pingTimeoutIn, requestCode);
1353                        }
1354                    } else {
1355                        pingCandidates.add(account);
1356                        if (isAccountPushed) {
1357                            if (mLowPingTimeoutMode.add(account.getJid().asBareJid())) {
1358                                Log.d(
1359                                        Config.LOGTAG,
1360                                        account.getJid().asBareJid()
1361                                                + ": entering low ping timeout mode");
1362                            }
1363                            return true;
1364                        } else if (msToNextPing <= 0) {
1365                            return true;
1366                        } else {
1367                            this.scheduleWakeUpCall(msToNextPing, requestCode);
1368                            if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
1369                                Log.d(
1370                                        Config.LOGTAG,
1371                                        account.getJid().asBareJid()
1372                                                + ": leaving low ping timeout mode");
1373                            }
1374                        }
1375                    }
1376                }
1377            } else if (account.getStatus() == Account.State.OFFLINE) {
1378                reconnectAccount(account, true, interactive);
1379            } else if (account.getStatus() == Account.State.CONNECTING) {
1380                final var connection = account.getXmppConnection();
1381                final var connectionDuration = connection.getConnectionDuration();
1382                final var discoDuration = connection.getDiscoDuration();
1383                final var connectionTimeout = Config.CONNECT_TIMEOUT * 1000L - connectionDuration;
1384                final var discoTimeout = Config.CONNECT_DISCO_TIMEOUT * 1000L - discoDuration;
1385                if (connectionTimeout < 0) {
1386                    connection.triggerConnectionTimeout();
1387                } else if (discoTimeout < 0) {
1388                    connection.sendDiscoTimeout();
1389                    scheduleWakeUpCall(discoTimeout, requestCode);
1390                } else {
1391                    scheduleWakeUpCall(Math.min(connectionTimeout, discoTimeout), requestCode);
1392                }
1393            } else {
1394                final boolean aggressive =
1395                        account.getStatus() == Account.State.SEE_OTHER_HOST
1396                                || hasJingleRtpConnection(account);
1397                if (account.getXmppConnection().getTimeToNextAttempt(aggressive) <= 0) {
1398                    reconnectAccount(account, true, interactive);
1399                }
1400            }
1401        }
1402        return false;
1403    }
1404
1405    private void toggleSoftDisabled(final boolean softDisabled) {
1406        for (final Account account : this.accounts) {
1407            if (account.isEnabled()) {
1408                if (account.setOption(Account.OPTION_SOFT_DISABLED, softDisabled)) {
1409                    updateAccount(account);
1410                }
1411            }
1412        }
1413    }
1414
1415    private void fetchServiceOutageStatus(final Account account) {
1416        final var sosUrl = account.getKey(Account.KEY_SOS_URL);
1417        if (Strings.isNullOrEmpty(sosUrl)) {
1418            return;
1419        }
1420        final var url = HttpUrl.parse(sosUrl);
1421        if (url == null) {
1422            return;
1423        }
1424        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching service outage " + url);
1425        Futures.addCallback(
1426                ServiceOutageStatus.fetch(getApplicationContext(), url),
1427                new FutureCallback<>() {
1428                    @Override
1429                    public void onSuccess(final ServiceOutageStatus sos) {
1430                        Log.d(Config.LOGTAG, "fetched " + sos);
1431                        account.setServiceOutageStatus(sos);
1432                        updateAccountUi();
1433                    }
1434
1435                    @Override
1436                    public void onFailure(@NonNull Throwable throwable) {
1437                        Log.d(Config.LOGTAG, "error fetching sos", throwable);
1438                    }
1439                },
1440                MoreExecutors.directExecutor());
1441    }
1442
1443    public boolean processUnifiedPushMessage(
1444            final Account account, final Jid transport, final Element push) {
1445        return unifiedPushBroker.processPushMessage(account, transport, push);
1446    }
1447
1448    public void reinitializeMuclumbusService() {
1449        mChannelDiscoveryService.initializeMuclumbusService();
1450    }
1451
1452    public void discoverChannels(
1453            String query,
1454            ChannelDiscoveryService.Method method,
1455            Map<Jid, Account> mucServices,
1456            ChannelDiscoveryService.OnChannelSearchResultsFound onChannelSearchResultsFound) {
1457        mChannelDiscoveryService.discover(
1458                Strings.nullToEmpty(query).trim(), method, mucServices, onChannelSearchResultsFound);
1459    }
1460
1461    public boolean isDataSaverDisabled() {
1462        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
1463            return true;
1464        }
1465        final ConnectivityManager connectivityManager = getSystemService(ConnectivityManager.class);
1466        return !Compatibility.isActiveNetworkMetered(connectivityManager)
1467                || Compatibility.getRestrictBackgroundStatus(connectivityManager)
1468                        == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
1469    }
1470
1471    private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {
1472        final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid);
1473        Message message = new Message(conversation, body, conversation.getNextEncryption());
1474        if (inReplyTo != null) {
1475            if (Emoticons.isEmoji(body.replaceAll("\\s", ""))) {
1476                final var aggregated = inReplyTo.getAggregatedReactions();
1477                final ImmutableSet.Builder<String> reactionBuilder = new ImmutableSet.Builder<>();
1478                reactionBuilder.addAll(aggregated.ourReactions);
1479                reactionBuilder.add(body.replaceAll("\\s", ""));
1480                sendReactions(inReplyTo, reactionBuilder.build());
1481                return;
1482            } else {
1483                message = inReplyTo.reply();
1484            }
1485            message.clearFallbacks("urn:xmpp:reply:0");
1486            message.setBody(body);
1487            message.setEncryption(conversation.getNextEncryption());
1488        }
1489        if (inReplyTo != null && inReplyTo.isPrivateMessage()) {
1490            Message.configurePrivateMessage(message, inReplyTo.getCounterpart());
1491        }
1492        message.markUnread();
1493        if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1494            getPgpEngine()
1495                    .encrypt(
1496                            message,
1497                            new UiCallback<Message>() {
1498                                @Override
1499                                public void success(Message message) {
1500                                    if (dismissAfterReply) {
1501                                        markRead((Conversation) message.getConversation(), true);
1502                                    } else {
1503                                        mNotificationService.pushFromDirectReply(message);
1504                                    }
1505                                }
1506
1507                                @Override
1508                                public void error(int errorCode, Message object) {}
1509
1510                                @Override
1511                                public void userInputRequired(PendingIntent pi, Message object) {}
1512                            });
1513        } else {
1514            sendMessage(message);
1515            if (dismissAfterReply) {
1516                markRead(conversation, true);
1517            } else {
1518                mNotificationService.pushFromDirectReply(message);
1519            }
1520        }
1521    }
1522
1523    private boolean dndOnSilentMode() {
1524        return getBooleanPreference(AppSettings.DND_ON_SILENT_MODE, R.bool.dnd_on_silent_mode);
1525    }
1526
1527    private boolean manuallyChangePresence() {
1528        return getBooleanPreference(
1529                AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
1530    }
1531
1532    private boolean treatVibrateAsSilent() {
1533        return getBooleanPreference(
1534                AppSettings.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent);
1535    }
1536
1537    private boolean awayWhenScreenLocked() {
1538        return getBooleanPreference(
1539                AppSettings.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off);
1540    }
1541
1542    private String getCompressPicturesPreference() {
1543        return getPreferences()
1544                .getString(
1545                        "picture_compression",
1546                        getResources().getString(R.string.picture_compression));
1547    }
1548
1549    private Presence.Status getTargetPresence() {
1550        if (dndOnSilentMode() && isPhoneSilenced()) {
1551            return Presence.Status.DND;
1552        } else if (awayWhenScreenLocked() && isScreenLocked()) {
1553            return Presence.Status.AWAY;
1554        } else {
1555            return Presence.Status.ONLINE;
1556        }
1557    }
1558
1559    public boolean isScreenLocked() {
1560        final KeyguardManager keyguardManager = getSystemService(KeyguardManager.class);
1561        final PowerManager powerManager = getSystemService(PowerManager.class);
1562        final boolean locked = keyguardManager != null && keyguardManager.isKeyguardLocked();
1563        final boolean interactive;
1564        try {
1565            interactive = powerManager != null && powerManager.isInteractive();
1566        } catch (final Exception e) {
1567            return false;
1568        }
1569        return locked || !interactive;
1570    }
1571
1572    private boolean isPhoneSilenced() {
1573        final NotificationManager notificationManager = getSystemService(NotificationManager.class);
1574        final int filter =
1575                notificationManager == null
1576                        ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN
1577                        : notificationManager.getCurrentInterruptionFilter();
1578        final boolean notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY;
1579        final AudioManager audioManager = getSystemService(AudioManager.class);
1580        final int ringerMode =
1581                audioManager == null
1582                        ? AudioManager.RINGER_MODE_NORMAL
1583                        : audioManager.getRingerMode();
1584        try {
1585            if (treatVibrateAsSilent()) {
1586                return notificationDnd || ringerMode != AudioManager.RINGER_MODE_NORMAL;
1587            } else {
1588                return notificationDnd || ringerMode == AudioManager.RINGER_MODE_SILENT;
1589            }
1590        } catch (final Throwable throwable) {
1591            Log.d(
1592                    Config.LOGTAG,
1593                    "platform bug in isPhoneSilenced (" + throwable.getMessage() + ")");
1594            return notificationDnd;
1595        }
1596    }
1597
1598    private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) {
1599        Log.d(Config.LOGTAG, "resetting all attempt counts");
1600        for (Account account : accounts) {
1601            if (account.hasErrorStatus() || reallyAll) {
1602                final XmppConnection connection = account.getXmppConnection();
1603                if (connection != null) {
1604                    connection.resetAttemptCount(retryImmediately);
1605                }
1606            }
1607            if (account.setShowErrorNotification(true)) {
1608                mDatabaseWriterExecutor.execute(() -> databaseBackend.updateAccount(account));
1609            }
1610        }
1611        mNotificationService.updateErrorNotification();
1612    }
1613
1614    private void dismissErrorNotifications() {
1615        for (final Account account : this.accounts) {
1616            if (account.hasErrorStatus()) {
1617                Log.d(
1618                        Config.LOGTAG,
1619                        account.getJid().asBareJid() + ": dismissing error notification");
1620                if (account.setShowErrorNotification(false)) {
1621                    mDatabaseWriterExecutor.execute(() -> databaseBackend.updateAccount(account));
1622                }
1623            }
1624        }
1625    }
1626
1627    private void expireOldMessages() {
1628        expireOldMessages(false);
1629    }
1630
1631    public void expireOldMessages(final boolean resetHasMessagesLeftOnServer) {
1632        mLastExpiryRun.set(SystemClock.elapsedRealtime());
1633        mDatabaseWriterExecutor.execute(
1634                () -> {
1635                    long timestamp = getAutomaticMessageDeletionDate();
1636                    if (timestamp > 0) {
1637                        databaseBackend.expireOldMessages(timestamp);
1638                        synchronized (XmppConnectionService.this.conversations) {
1639                            for (Conversation conversation :
1640                                    XmppConnectionService.this.conversations) {
1641                                conversation.expireOldMessages(timestamp);
1642                                if (resetHasMessagesLeftOnServer) {
1643                                    conversation.messagesLoaded.set(true);
1644                                    conversation.setHasMessagesLeftOnServer(true);
1645                                }
1646                            }
1647                        }
1648                        updateConversationUi();
1649                    }
1650                });
1651    }
1652
1653    public boolean hasInternetConnection() {
1654        final ConnectivityManager cm =
1655                ContextCompat.getSystemService(this, ConnectivityManager.class);
1656        if (cm == null) {
1657            return true; // if internet connection can not be checked it is probably best to just
1658            // try
1659        }
1660        try {
1661            if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
1662                final Network activeNetwork = cm.getActiveNetwork();
1663                final NetworkCapabilities capabilities =
1664                        activeNetwork == null ? null : cm.getNetworkCapabilities(activeNetwork);
1665                return capabilities != null
1666                        && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
1667            } else {
1668                final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
1669                return networkInfo != null
1670                        && (networkInfo.isConnected()
1671                                || networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET);
1672            }
1673        } catch (final RuntimeException e) {
1674            Log.d(Config.LOGTAG, "unable to check for internet connection", e);
1675            return true; // if internet connection can not be checked it is probably best to just
1676            // try
1677        }
1678    }
1679
1680    @SuppressLint("TrulyRandom")
1681    @Override
1682    public void onCreate() {
1683        com.cheogram.android.AndroidLoggingHandler.reset(new com.cheogram.android.AndroidLoggingHandler());
1684        java.util.logging.Logger.getLogger("").setLevel(java.util.logging.Level.FINEST);
1685        LibIdnXmppStringprep.setup();
1686        emojiSearch = new EmojiSearch(this);
1687        setTheme(R.style.Theme_Conversations3);
1688        ThemeHelper.applyCustomColors(this);
1689        if (Compatibility.twentySix()) {
1690            mNotificationService.initializeChannels();
1691        }
1692        mChannelDiscoveryService.initializeMuclumbusService();
1693        mForceDuringOnCreate.set(Compatibility.twentySix());
1694        toggleForegroundService();
1695        this.destroyed = false;
1696        OmemoSetting.load(this);
1697        try {
1698            Security.insertProviderAt(Conscrypt.newProvider(), 1);
1699        } catch (Throwable throwable) {
1700            Log.e(Config.LOGTAG, "unable to initialize security provider", throwable);
1701        }
1702        Resolver.init(this);
1703        updateMemorizingTrustManager();
1704        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
1705        final int cacheSize = maxMemory / 8;
1706        this.mDrawableCache = new LruCache<String, Drawable>(cacheSize) {
1707            @Override
1708            protected int sizeOf(final String key, final Drawable drawable) {
1709                if (drawable instanceof BitmapDrawable) {
1710                    Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
1711                    if (bitmap == null) return 1024;
1712
1713                    return bitmap.getByteCount() / 1024;
1714                } else if (drawable instanceof AvatarService.TextDrawable) {
1715                    return 50;
1716                } else {
1717                    return drawable.getIntrinsicWidth() * drawable.getIntrinsicHeight() * 40 / 1024;
1718                }
1719            }
1720        };
1721        if (mLastActivity == 0) {
1722            mLastActivity =
1723                    getPreferences().getLong(SETTING_LAST_ACTIVITY_TS, System.currentTimeMillis());
1724        }
1725
1726        Log.d(Config.LOGTAG, "initializing database...");
1727        this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
1728        Log.d(Config.LOGTAG, "restoring accounts...");
1729        this.accounts = databaseBackend.getAccounts();
1730        for (Account account : this.accounts) {
1731            final int color = getPreferences().getInt("account_color:" + account.getUuid(), 0);
1732            if (color != 0) account.setColor(color);
1733        }
1734        final SharedPreferences.Editor editor = getPreferences().edit();
1735        final boolean hasEnabledAccounts = hasEnabledAccounts();
1736        editor.putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
1737        editor.apply();
1738        toggleSetProfilePictureActivity(hasEnabledAccounts);
1739        reconfigurePushDistributor();
1740
1741        if (CallIntegration.hasSystemFeature(this)) {
1742            CallIntegrationConnectionService.togglePhoneAccountsAsync(this, this.accounts);
1743        }
1744
1745        restoreFromDatabase();
1746
1747        if (QuickConversationsService.isContactListIntegration(this)
1748                && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
1749                        == PackageManager.PERMISSION_GRANTED) {
1750            startContactObserver();
1751        }
1752        FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath);
1753        if (Compatibility.hasStoragePermission(this)) {
1754            Log.d(Config.LOGTAG, "starting file observer");
1755            FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::startWatching);
1756            FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
1757        }
1758        if (Config.supportOpenPgp()) {
1759            this.pgpServiceConnection =
1760                    new OpenPgpServiceConnection(
1761                            this,
1762                            "org.sufficientlysecure.keychain",
1763                            new OpenPgpServiceConnection.OnBound() {
1764                                @Override
1765                                public void onBound(final IOpenPgpService2 service) {
1766                                    for (Account account : accounts) {
1767                                        final PgpDecryptionService pgp =
1768                                                account.getPgpDecryptionService();
1769                                        if (pgp != null) {
1770                                            pgp.continueDecryption(true);
1771                                        }
1772                                    }
1773                                }
1774
1775                                @Override
1776                                public void onError(final Exception exception) {
1777                                    Log.e(
1778                                            Config.LOGTAG,
1779                                            "could not bind to OpenKeyChain",
1780                                            exception);
1781                                }
1782                            });
1783            this.pgpServiceConnection.bindToService();
1784        }
1785
1786        final PowerManager powerManager = getSystemService(PowerManager.class);
1787        if (powerManager != null) {
1788            this.wakeLock =
1789                    powerManager.newWakeLock(
1790                            PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service");
1791        }
1792
1793        toggleForegroundService();
1794        updateUnreadCountBadge();
1795        toggleScreenEventReceiver();
1796        final IntentFilter systemBroadcastFilter = new IntentFilter();
1797        scheduleNextIdlePing();
1798        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1799            systemBroadcastFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
1800        }
1801        systemBroadcastFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
1802        ContextCompat.registerReceiver(
1803                this,
1804                this.mInternalEventReceiver,
1805                systemBroadcastFilter,
1806                ContextCompat.RECEIVER_NOT_EXPORTED);
1807        final IntentFilter exportedBroadcastFilter = new IntentFilter();
1808        exportedBroadcastFilter.addAction(TorServiceUtils.ACTION_STATUS);
1809        ContextCompat.registerReceiver(
1810                this,
1811                this.mInternalRestrictedEventReceiver,
1812                exportedBroadcastFilter,
1813                ContextCompat.RECEIVER_EXPORTED);
1814        mForceDuringOnCreate.set(false);
1815        toggleForegroundService();
1816        rescanStickers();
1817        cleanupCache();
1818        internalPingExecutor.scheduleWithFixedDelay(
1819                this::manageAccountConnectionStatesInternal, 10, 10, TimeUnit.SECONDS);
1820        final SharedPreferences sharedPreferences =
1821                androidx.preference.PreferenceManager.getDefaultSharedPreferences(this);
1822        sharedPreferences.registerOnSharedPreferenceChangeListener(
1823                new SharedPreferences.OnSharedPreferenceChangeListener() {
1824                    @Override
1825                    public void onSharedPreferenceChanged(
1826                            SharedPreferences sharedPreferences, @Nullable String key) {
1827                        Log.d(Config.LOGTAG, "preference '" + key + "' has changed");
1828                        if (AppSettings.KEEP_FOREGROUND_SERVICE.equals(key)) {
1829                            toggleForegroundService();
1830                        }
1831                    }
1832                });
1833    }
1834
1835    private void checkForDeletedFiles() {
1836        if (destroyed) {
1837            Log.d(
1838                    Config.LOGTAG,
1839                    "Do not check for deleted files because service has been destroyed");
1840            return;
1841        }
1842        final long start = SystemClock.elapsedRealtime();
1843        final List<DatabaseBackend.FilePathInfo> relativeFilePaths =
1844                databaseBackend.getFilePathInfo();
1845        final List<DatabaseBackend.FilePathInfo> changed = new ArrayList<>();
1846        for (final DatabaseBackend.FilePathInfo filePath : relativeFilePaths) {
1847            if (destroyed) {
1848                Log.d(
1849                        Config.LOGTAG,
1850                        "Stop checking for deleted files because service has been destroyed");
1851                return;
1852            }
1853            final File file = fileBackend.getFileForPath(filePath.path);
1854            if (filePath.setDeleted(!file.exists())) {
1855                changed.add(filePath);
1856            }
1857        }
1858        final long duration = SystemClock.elapsedRealtime() - start;
1859        Log.d(
1860                Config.LOGTAG,
1861                "found "
1862                        + changed.size()
1863                        + " changed files on start up. total="
1864                        + relativeFilePaths.size()
1865                        + ". ("
1866                        + duration
1867                        + "ms)");
1868        if (changed.size() > 0) {
1869            databaseBackend.markFilesAsChanged(changed);
1870            markChangedFiles(changed);
1871        }
1872    }
1873
1874    public void startContactObserver() {
1875        getContentResolver()
1876                .registerContentObserver(
1877                        ContactsContract.Contacts.CONTENT_URI,
1878                        true,
1879                        new ContentObserver(null) {
1880                            @Override
1881                            public void onChange(boolean selfChange) {
1882                                super.onChange(selfChange);
1883                                if (restoredFromDatabaseLatch.getCount() == 0) {
1884                                    loadPhoneContacts();
1885                                }
1886                            }
1887                        });
1888    }
1889
1890    @Override
1891    public void onTrimMemory(int level) {
1892        super.onTrimMemory(level);
1893        if (level >= TRIM_MEMORY_COMPLETE) {
1894            Log.d(Config.LOGTAG, "clear cache due to low memory");
1895            getDrawableCache().evictAll();
1896        }
1897    }
1898
1899    @Override
1900    public void onDestroy() {
1901        try {
1902            unregisterReceiver(this.mInternalEventReceiver);
1903            unregisterReceiver(this.mInternalRestrictedEventReceiver);
1904            unregisterReceiver(this.mInternalScreenEventReceiver);
1905        } catch (final IllegalArgumentException e) {
1906            // ignored
1907        }
1908        destroyed = false;
1909        fileObserver.stopWatching();
1910        internalPingExecutor.shutdown();
1911        super.onDestroy();
1912    }
1913
1914    public void restartFileObserver() {
1915        Log.d(Config.LOGTAG, "restarting file observer");
1916        FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::restartWatching);
1917        FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
1918    }
1919
1920    public void toggleScreenEventReceiver() {
1921        if (awayWhenScreenLocked() && !manuallyChangePresence()) {
1922            final IntentFilter filter = new IntentFilter();
1923            filter.addAction(Intent.ACTION_SCREEN_ON);
1924            filter.addAction(Intent.ACTION_SCREEN_OFF);
1925            filter.addAction(Intent.ACTION_USER_PRESENT);
1926            registerReceiver(this.mInternalScreenEventReceiver, filter);
1927        } else {
1928            try {
1929                unregisterReceiver(this.mInternalScreenEventReceiver);
1930            } catch (IllegalArgumentException e) {
1931                // ignored
1932            }
1933        }
1934    }
1935
1936    public void toggleForegroundService() {
1937        toggleForegroundService(false, false);
1938    }
1939
1940    public void setOngoingCall(
1941            AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
1942        ongoingCall.set(new OngoingCall(id, media, reconnecting));
1943        toggleForegroundService(false, true);
1944    }
1945
1946    public void removeOngoingCall() {
1947        ongoingCall.set(null);
1948        toggleForegroundService(false, false);
1949    }
1950
1951    private void toggleForegroundService(boolean force, boolean needMic) {
1952        final boolean status;
1953        final OngoingCall ongoing = ongoingCall.get();
1954        final boolean ongoingVideoTranscoding = mOngoingVideoTranscoding.get();
1955        final int id;
1956        if (force
1957                || mForceDuringOnCreate.get()
1958                || ongoingVideoTranscoding
1959                || ongoing != null
1960                || (appSettings.isKeepForegroundService() && hasEnabledAccounts())) {
1961            final Notification notification;
1962            if (ongoing != null && !diallerIntegrationActive.get()) {
1963                notification = this.mNotificationService.getOngoingCallNotification(ongoing);
1964                id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
1965                startForegroundOrCatch(id, notification, true);
1966            } else if (ongoingVideoTranscoding) {
1967                notification = this.mNotificationService.getIndeterminateVideoTranscoding();
1968                id = NotificationService.ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID;
1969                startForegroundOrCatch(id, notification, false);
1970            } else {
1971                notification = this.mNotificationService.createForegroundNotification();
1972                id = NotificationService.FOREGROUND_NOTIFICATION_ID;
1973                startForegroundOrCatch(id, notification, needMic || ongoing != null || diallerIntegrationActive.get());
1974            }
1975            mNotificationService.notify(id, notification);
1976            status = true;
1977        } else {
1978            id = 0;
1979            stopForeground(true);
1980            status = false;
1981        }
1982
1983        for (final int toBeRemoved :
1984                Collections2.filter(
1985                        Arrays.asList(
1986                                NotificationService.FOREGROUND_NOTIFICATION_ID,
1987                                NotificationService.ONGOING_CALL_NOTIFICATION_ID,
1988                                NotificationService.ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID),
1989                        i -> i != id)) {
1990            mNotificationService.cancel(toBeRemoved);
1991        }
1992        Log.d(
1993                Config.LOGTAG,
1994                "ForegroundService: " + (status ? "on" : "off") + ", notification: " + id);
1995    }
1996
1997    private void startForegroundOrCatch(
1998            final int id, final Notification notification, final boolean requireMicrophone) {
1999        try {
2000            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
2001                final int foregroundServiceType;
2002                if (requireMicrophone
2003                        && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
2004                                == PackageManager.PERMISSION_GRANTED) {
2005                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
2006                    Log.d(Config.LOGTAG, "defaulting to microphone foreground service type");
2007                } else if (getSystemService(PowerManager.class)
2008                        .isIgnoringBatteryOptimizations(getPackageName())) {
2009                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED;
2010                } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
2011                        == PackageManager.PERMISSION_GRANTED) {
2012                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
2013                } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
2014                        == PackageManager.PERMISSION_GRANTED) {
2015                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
2016                } else {
2017                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
2018                    Log.w(Config.LOGTAG, "falling back to special use foreground service type");
2019                }
2020
2021                startForeground(id, notification, foregroundServiceType);
2022            } else {
2023                startForeground(id, notification);
2024            }
2025        } catch (final IllegalStateException | SecurityException e) {
2026            Log.e(Config.LOGTAG, "Could not start foreground service", e);
2027        }
2028    }
2029
2030    public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() {
2031        return !mOngoingVideoTranscoding.get()
2032                && ongoingCall.get() == null
2033                && appSettings.isKeepForegroundService()
2034                && hasEnabledAccounts();
2035    }
2036
2037    @Override
2038    public void onTaskRemoved(final Intent rootIntent) {
2039        super.onTaskRemoved(rootIntent);
2040        if ((appSettings.isKeepForegroundService() && hasEnabledAccounts())
2041                || mOngoingVideoTranscoding.get()
2042                || ongoingCall.get() != null) {
2043            Log.d(Config.LOGTAG, "ignoring onTaskRemoved because foreground service is activated");
2044        } else {
2045            this.logoutAndSave(false);
2046        }
2047    }
2048
2049    private void logoutAndSave(boolean stop) {
2050        int activeAccounts = 0;
2051        for (final Account account : accounts) {
2052            if (account.isConnectionEnabled()) {
2053                databaseBackend.writeRoster(account.getRoster());
2054                activeAccounts++;
2055            }
2056            if (account.getXmppConnection() != null) {
2057                new Thread(() -> disconnect(account, false)).start();
2058            }
2059        }
2060        if (stop || activeAccounts == 0) {
2061            Log.d(Config.LOGTAG, "good bye");
2062            stopSelf();
2063        }
2064    }
2065
2066    private void schedulePostConnectivityChange() {
2067        final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
2068        if (alarmManager == null) {
2069            return;
2070        }
2071        final long triggerAtMillis =
2072                SystemClock.elapsedRealtime()
2073                        + (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL * 1000);
2074        final Intent intent = new Intent(this, SystemEventReceiver.class);
2075        intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE);
2076        try {
2077            final PendingIntent pendingIntent =
2078                    PendingIntent.getBroadcast(
2079                            this,
2080                            1,
2081                            intent,
2082                            s()
2083                                    ? PendingIntent.FLAG_IMMUTABLE
2084                                            | PendingIntent.FLAG_UPDATE_CURRENT
2085                                    : PendingIntent.FLAG_UPDATE_CURRENT);
2086            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
2087                alarmManager.setAndAllowWhileIdle(
2088                        AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
2089            } else {
2090                alarmManager.set(
2091                        AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
2092            }
2093        } catch (RuntimeException e) {
2094            Log.e(Config.LOGTAG, "unable to schedule alarm for post connectivity change", e);
2095        }
2096    }
2097
2098    public void scheduleWakeUpCall(final int seconds, final int requestCode) {
2099        scheduleWakeUpCall((seconds < 0 ? 1 : seconds + 1) * 1000L, requestCode);
2100    }
2101
2102    public void scheduleWakeUpCall(final long milliSeconds, final int requestCode) {
2103        final var timeToWake = SystemClock.elapsedRealtime() + milliSeconds;
2104        final var alarmManager = getSystemService(AlarmManager.class);
2105        final Intent intent = new Intent(this, SystemEventReceiver.class);
2106        intent.setAction(ACTION_PING);
2107        try {
2108            final PendingIntent pendingIntent =
2109                    PendingIntent.getBroadcast(
2110                            this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE);
2111            alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
2112        } catch (final RuntimeException e) {
2113            Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e);
2114        }
2115    }
2116
2117    private void scheduleNextIdlePing() {
2118        long timeUntilWake = Config.IDLE_PING_INTERVAL * 1000;
2119        final var now = System.currentTimeMillis();
2120        for (final var message : mScheduledMessages.values()) {
2121            if (message.getTimeSent() <= now) continue; // Just in case
2122            if (message.getTimeSent() - now < timeUntilWake) timeUntilWake = message.getTimeSent() - now;
2123        }
2124        final var timeToWake = SystemClock.elapsedRealtime() + timeUntilWake;
2125        final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
2126        if (alarmManager == null) {
2127            Log.d(Config.LOGTAG, "no alarm manager?");
2128            return;
2129        }
2130        final Intent intent = new Intent(this, SystemEventReceiver.class);
2131        intent.setAction(ACTION_IDLE_PING);
2132        try {
2133            final PendingIntent pendingIntent =
2134                    PendingIntent.getBroadcast(
2135                            this,
2136                            0,
2137                            intent,
2138                            s()
2139                                    ? PendingIntent.FLAG_IMMUTABLE
2140                                            | PendingIntent.FLAG_UPDATE_CURRENT
2141                                    : PendingIntent.FLAG_UPDATE_CURRENT);
2142            alarmManager.setAndAllowWhileIdle(
2143                    AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
2144        } catch (RuntimeException e) {
2145            Log.d(Config.LOGTAG, "unable to schedule alarm for idle ping", e);
2146        }
2147    }
2148
2149    public XmppConnection createConnection(final Account account) {
2150        final XmppConnection connection = new XmppConnection(account, this);
2151        connection.setOnStatusChangedListener(this.statusListener);
2152        connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket));
2153        connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
2154        connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
2155        connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService);
2156        AxolotlService axolotlService = account.getAxolotlService();
2157        if (axolotlService != null) {
2158            connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
2159        }
2160        return connection;
2161    }
2162
2163    public void sendChatState(Conversation conversation) {
2164        if (sendChatStates()) {
2165            final var packet = mMessageGenerator.generateChatState(conversation);
2166            sendMessagePacket(conversation.getAccount(), packet);
2167        }
2168    }
2169
2170    private void sendFileMessage(
2171            final Message message, final boolean delay, final boolean forceP2P, final Runnable cb) {
2172        final var account = message.getConversation().getAccount();
2173        Log.d(
2174                Config.LOGTAG,
2175                account.getJid().asBareJid() + ": send file message. forceP2P=" + forceP2P);
2176        if ((account.httpUploadAvailable(fileBackend.getFile(message, false).getSize())
2177                        || message.getConversation().getMode() == Conversation.MODE_MULTI)
2178                && !forceP2P) {
2179            mHttpConnectionManager.createNewUploadConnection(message, delay, cb);
2180        } else {
2181            mJingleConnectionManager.startJingleFileTransfer(message);
2182            if (cb != null) cb.run();
2183        }
2184    }
2185
2186    public void sendMessage(final Message message) {
2187        sendMessage(message, false, false, false, false, null);
2188    }
2189
2190    public void sendMessage(final Message message, final Runnable cb) {
2191        sendMessage(message, false, false, false, false, cb);
2192    }
2193
2194    private void sendMessage(final Message message, final boolean resend, final boolean previewedLinks, final boolean delay, final Runnable cb) {
2195        sendMessage(message, resend, previewedLinks, delay, false, cb);
2196    }
2197
2198    private void sendMessage(
2199            final Message message,
2200            final boolean resend,
2201            final boolean previewedLinks,
2202            final boolean delay,
2203            final boolean forceP2P,
2204            final Runnable cb) {
2205        final Account account = message.getConversation().getAccount();
2206        if (account.setShowErrorNotification(true)) {
2207            databaseBackend.updateAccount(account);
2208            mNotificationService.updateErrorNotification();
2209        }
2210        final Conversation conversation = (Conversation) message.getConversation();
2211        account.deactivateGracePeriod();
2212
2213        if (QuickConversationsService.isQuicksy()
2214                && conversation.getMode() == Conversation.MODE_SINGLE) {
2215            final Contact contact = conversation.getContact();
2216            if (!contact.showInRoster() && contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) {
2217                Log.d(
2218                        Config.LOGTAG,
2219                        account.getJid().asBareJid()
2220                                + ": adding "
2221                                + contact.getJid()
2222                                + " on sending message");
2223                createContact(contact, true);
2224            }
2225        }
2226
2227        im.conversations.android.xmpp.model.stanza.Message packet = null;
2228        final boolean addToConversation = !message.edited() && message.getRawBody() != null;
2229        boolean saveInDb = addToConversation;
2230        message.setStatus(Message.STATUS_WAITING);
2231
2232        if (message.getEncryption() != Message.ENCRYPTION_NONE
2233                && conversation.getMode() == Conversation.MODE_MULTI
2234                && conversation.isPrivateAndNonAnonymous()) {
2235            if (conversation.setAttribute(
2236                    Conversation.ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, true)) {
2237                databaseBackend.updateConversation(conversation);
2238            }
2239        }
2240
2241        final boolean inProgressJoin = isJoinInProgress(conversation);
2242
2243        if (message.getCounterpart() == null && !message.isPrivateMessage()) {
2244            message.setCounterpart(message.getConversation().getJid().asBareJid());
2245        }
2246
2247        boolean waitForPreview = false;
2248        if (getPreferences().getBoolean("send_link_previews", true) && !previewedLinks && !message.needsUploading() && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
2249            message.clearLinkDescriptions();
2250            final List<URI> links = message.getLinks();
2251            if (!links.isEmpty()) {
2252                waitForPreview = true;
2253                if (account.isOnlineAndConnected()) {
2254                    FILE_ATTACHMENT_EXECUTOR.execute(() -> {
2255                        for (URI link : links) {
2256                            if ("https".equals(link.getScheme())) {
2257                                try {
2258                                    HttpUrl url = HttpUrl.parse(link.toString());
2259                                    OkHttpClient http = getHttpConnectionManager().buildHttpClient(url, account, 5, false);
2260                                    final var request = new okhttp3.Request.Builder().url(url).head().build();
2261                                    okhttp3.Response response = null;
2262                                    if ("www.amazon.com".equals(link.getHost()) || "www.amazon.ca".equals(link.getHost())) {
2263                                        // Amazon blocks HEAD
2264                                        response = new okhttp3.Response.Builder().request(request).protocol(okhttp3.Protocol.HTTP_1_1).code(200).message("OK").addHeader("Content-Type", "text/html").build();
2265                                    } else {
2266                                        response = http.newCall(request).execute();
2267                                    }
2268                                    final String mimeType = response.header("Content-Type") == null ? "" : response.header("Content-Type");
2269                                    final boolean image = mimeType.startsWith("image/");
2270                                    final boolean audio = mimeType.startsWith("audio/");
2271                                    final boolean video = mimeType.startsWith("video/");
2272                                    final boolean pdf = mimeType.equals("application/pdf");
2273                                    final boolean html = mimeType.startsWith("text/html") || mimeType.startsWith("application/xhtml+xml");
2274                                    if (response.isSuccessful() && (image || audio || video || pdf)) {
2275                                        Message.FileParams params = message.getFileParams();
2276                                        params.url = url.toString();
2277                                        if (response.header("Content-Length") != null) params.size = Long.parseLong(response.header("Content-Length"), 10);
2278                                        if (!Message.configurePrivateFileMessage(message)) {
2279                                            message.setType(image ? Message.TYPE_IMAGE : Message.TYPE_FILE);
2280                                        }
2281                                        params.setName(HttpConnectionManager.extractFilenameFromResponse(response));
2282
2283                                        if (link.toString().equals(message.getRawBody())) {
2284                                            Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
2285                                            fallback.addChild("body", "urn:xmpp:fallback:0");
2286                                            message.addPayload(fallback);
2287                                        } else if (message.getRawBody().indexOf(link.toString()) >= 0) {
2288                                            // Part of the real body, not just a fallback
2289                                            Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
2290                                            fallback.addChild("body", "urn:xmpp:fallback:0")
2291                                                .setAttribute("start", "0")
2292                                                .setAttribute("end", "0");
2293                                            message.addPayload(fallback);
2294                                        }
2295
2296                                        final int encryption = message.getEncryption();
2297                                        getHttpConnectionManager().createNewDownloadConnection(message, false, (file) -> {
2298                                            message.setEncryption(encryption);
2299                                            synchronized (message.getConversation()) {
2300                                                if (message.getStatus() == Message.STATUS_WAITING) sendMessage(message, true, true, false, cb);
2301                                            }
2302                                        });
2303                                        return;
2304                                    } else if (response.isSuccessful() && html) {
2305                                        Semaphore waiter = new Semaphore(0);
2306                                        OpenGraphParser.Builder openGraphBuilder = new OpenGraphParser.Builder(new OpenGraphCallback() {
2307                                            @Override
2308                                            public void onPostResponse(OpenGraphResult result) {
2309                                                Element rdf = new Element("Description", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
2310                                                rdf.setAttribute("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
2311                                                rdf.setAttribute("rdf:about", link.toString());
2312                                                if (result.getTitle() != null && !"".equals(result.getTitle())) {
2313                                                    rdf.addChild("title", "https://ogp.me/ns#").setContent(result.getTitle());
2314                                                }
2315                                                if (result.getDescription() != null && !"".equals(result.getDescription())) {
2316                                                    rdf.addChild("description", "https://ogp.me/ns#").setContent(result.getDescription());
2317                                                }
2318                                                if (result.getUrl() != null) {
2319                                                    rdf.addChild("url", "https://ogp.me/ns#").setContent(result.getUrl());
2320                                                }
2321                                                if (result.getImage() != null) {
2322                                                    rdf.addChild("image", "https://ogp.me/ns#").setContent(result.getImage());
2323                                                }
2324                                                if (result.getType() != null) {
2325                                                    rdf.addChild("type", "https://ogp.me/ns#").setContent(result.getType());
2326                                                }
2327                                                if (result.getSiteName() != null) {
2328                                                    rdf.addChild("site_name", "https://ogp.me/ns#").setContent(result.getSiteName());
2329                                                }
2330                                                if (result.getVideo() != null) {
2331                                                    rdf.addChild("video", "https://ogp.me/ns#").setContent(result.getVideo());
2332                                                }
2333                                                message.addPayload(rdf);
2334                                                waiter.release();
2335                                            }
2336
2337                                            public void onError(String error) {
2338                                                waiter.release();
2339                                            }
2340                                        })
2341                                            .showNullOnEmpty(true)
2342                                            .maxBodySize(90000)
2343                                            .timeout(5000);
2344                                        if (useTorToConnect()) {
2345                                            openGraphBuilder = openGraphBuilder.jsoupProxy(new JsoupProxy("127.0.0.1", 8118));
2346                                        }
2347                                        openGraphBuilder.build().parse(link.toString());
2348                                        waiter.tryAcquire(10L, TimeUnit.SECONDS);
2349                                    }
2350                                } catch (final IOException | InterruptedException e) {  }
2351                            }
2352                        }
2353                        synchronized (message.getConversation()) {
2354                            if (message.getStatus() == Message.STATUS_WAITING) sendMessage(message, true, true, false, cb);
2355                        }
2356                    });
2357                }
2358            }
2359        }
2360
2361        boolean passedCbOn = false;
2362        if (account.isOnlineAndConnected() && !inProgressJoin && !waitForPreview && message.getTimeSent() <= System.currentTimeMillis()) {
2363            switch (message.getEncryption()) {
2364                case Message.ENCRYPTION_NONE:
2365                    if (message.needsUploading()) {
2366                        if (account.httpUploadAvailable(
2367                                        fileBackend.getFile(message, false).getSize())
2368                                || conversation.getMode() == Conversation.MODE_MULTI
2369                                || message.fixCounterpart()) {
2370                            this.sendFileMessage(message, delay, forceP2P, cb);
2371                            passedCbOn = true;
2372                        } else {
2373                            break;
2374                        }
2375                    } else {
2376                        packet = mMessageGenerator.generateChat(message);
2377                    }
2378                    break;
2379                case Message.ENCRYPTION_PGP:
2380                case Message.ENCRYPTION_DECRYPTED:
2381                    if (message.needsUploading()) {
2382                        if (account.httpUploadAvailable(
2383                                        fileBackend.getFile(message, false).getSize())
2384                                || conversation.getMode() == Conversation.MODE_MULTI
2385                                || message.fixCounterpart()) {
2386                            this.sendFileMessage(message, delay, forceP2P, cb);
2387                            passedCbOn = true;
2388                        } else {
2389                            break;
2390                        }
2391                    } else {
2392                        packet = mMessageGenerator.generatePgpChat(message);
2393                    }
2394                    break;
2395                case Message.ENCRYPTION_AXOLOTL:
2396                    message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
2397                    if (message.needsUploading()) {
2398                        if (account.httpUploadAvailable(
2399                                        fileBackend.getFile(message, false).getSize())
2400                                || conversation.getMode() == Conversation.MODE_MULTI
2401                                || message.fixCounterpart()) {
2402                            this.sendFileMessage(message, delay, forceP2P, cb);
2403                            passedCbOn = true;
2404                        } else {
2405                            break;
2406                        }
2407                    } else {
2408                        XmppAxolotlMessage axolotlMessage =
2409                                account.getAxolotlService().fetchAxolotlMessageFromCache(message);
2410                        if (axolotlMessage == null) {
2411                            account.getAxolotlService().preparePayloadMessage(message, delay);
2412                        } else {
2413                            packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
2414                        }
2415                    }
2416                    break;
2417            }
2418            if (packet != null) {
2419                if (account.getXmppConnection().getFeatures().sm()
2420                        || (conversation.getMode() == Conversation.MODE_MULTI
2421                                && message.getCounterpart().isBareJid())) {
2422                    message.setStatus(Message.STATUS_UNSEND);
2423                } else {
2424                    message.setStatus(Message.STATUS_SEND);
2425                }
2426            }
2427        } else {
2428            switch (message.getEncryption()) {
2429                case Message.ENCRYPTION_DECRYPTED:
2430                    if (!message.needsUploading()) {
2431                        String pgpBody = message.getEncryptedBody();
2432                        String decryptedBody = message.getBody();
2433                        message.setBody(pgpBody); // TODO might throw NPE
2434                        message.setEncryption(Message.ENCRYPTION_PGP);
2435                        if (message.edited()) {
2436                            message.setBody(decryptedBody);
2437                            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
2438                            if (!databaseBackend.updateMessage(message, message.getEditedId())) {
2439                                Log.e(Config.LOGTAG, "error updated message in DB after edit");
2440                            }
2441                            updateConversationUi();
2442                            if (!waitForPreview && cb != null) cb.run();
2443                            return;
2444                        } else {
2445                            databaseBackend.createMessage(message);
2446                            saveInDb = false;
2447                            message.setBody(decryptedBody);
2448                            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
2449                        }
2450                    }
2451                    break;
2452                case Message.ENCRYPTION_AXOLOTL:
2453                    message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
2454                    break;
2455            }
2456        }
2457
2458        synchronized (mScheduledMessages) {
2459            if (message.getTimeSent() > System.currentTimeMillis()) {
2460                mScheduledMessages.put(message.getUuid(), message);
2461                scheduleNextIdlePing();
2462            } else {
2463                mScheduledMessages.remove(message.getUuid());
2464            }
2465        }
2466
2467        boolean mucMessage =
2468                conversation.getMode() == Conversation.MODE_MULTI && !message.isPrivateMessage();
2469        if (mucMessage) {
2470            message.setCounterpart(conversation.getMucOptions().getSelf().getFullJid());
2471        }
2472
2473        if (resend) {
2474            if (packet != null && addToConversation) {
2475                if (account.getXmppConnection().getFeatures().sm() || mucMessage) {
2476                    markMessage(message, Message.STATUS_UNSEND);
2477                } else {
2478                    markMessage(message, Message.STATUS_SEND);
2479                }
2480            }
2481        } else {
2482            if (addToConversation) {
2483                conversation.add(message);
2484            }
2485            if (saveInDb) {
2486                databaseBackend.createMessage(message);
2487            } else if (message.edited()) {
2488                if (!databaseBackend.updateMessage(message, message.getEditedId())) {
2489                    Log.e(Config.LOGTAG, "error updated message in DB after edit");
2490                }
2491            }
2492            updateConversationUi();
2493        }
2494        if (packet != null) {
2495            if (delay) {
2496                mMessageGenerator.addDelay(packet, message.getTimeSent());
2497            }
2498            if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
2499                if (this.sendChatStates()) {
2500                    packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
2501                }
2502            }
2503            sendMessagePacket(account, packet);
2504            if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.hasCustomEmoji()) {
2505                if (message.getConversation() instanceof Conversation) presenceToMuc((Conversation) message.getConversation());
2506            }
2507        }
2508        if (!waitForPreview && !passedCbOn && cb != null) cb.run();
2509    }
2510
2511    private boolean isJoinInProgress(final Conversation conversation) {
2512        final Account account = conversation.getAccount();
2513        synchronized (account.inProgressConferenceJoins) {
2514            if (conversation.getMode() == Conversational.MODE_MULTI) {
2515                final boolean inProgress = account.inProgressConferenceJoins.contains(conversation);
2516                final boolean pending = account.pendingConferenceJoins.contains(conversation);
2517                final boolean inProgressJoin = inProgress || pending;
2518                if (inProgressJoin) {
2519                    Log.d(
2520                            Config.LOGTAG,
2521                            account.getJid().asBareJid()
2522                                    + ": holding back message to group. inProgress="
2523                                    + inProgress
2524                                    + ", pending="
2525                                    + pending);
2526                }
2527                return inProgressJoin;
2528            } else {
2529                return false;
2530            }
2531        }
2532    }
2533
2534    private void sendUnsentMessages(final Conversation conversation) {
2535        synchronized (conversation) {
2536            conversation.findWaitingMessages(message -> resendMessage(message, true));
2537        }
2538    }
2539
2540    public void resendMessage(final Message message, final boolean delay) {
2541        sendMessage(message, true, false, delay, false, null);
2542    }
2543
2544    public void resendMessage(final Message message, final boolean delay, final Runnable cb) {
2545        sendMessage(message, true, false, delay, false, cb);
2546    }
2547
2548    public void resendMessage(final Message message, final boolean delay, final boolean previewedLinks) {
2549        sendMessage(message, true, previewedLinks, delay, false, null);
2550    }
2551
2552    public Pair<Account,Account> onboardingIncomplete() {
2553        if (getAccounts().size() != 2) return null;
2554        Account onboarding = null;
2555        Account newAccount = null;
2556        for (final Account account : getAccounts()) {
2557            if (account.getJid().getDomain().equals(Config.ONBOARDING_DOMAIN)) {
2558                onboarding = account;
2559            } else {
2560                newAccount = account;
2561            }
2562        }
2563
2564        if (onboarding != null && newAccount != null) {
2565            return new Pair<>(onboarding, newAccount);
2566        }
2567
2568        return null;
2569    }
2570
2571    public boolean isOnboarding() {
2572        return getAccounts().size() == 1 && getAccounts().get(0).getJid().getDomain().equals(Config.ONBOARDING_DOMAIN);
2573    }
2574
2575    public void requestEasyOnboardingInvite(
2576            final Account account, final EasyOnboardingInvite.OnInviteRequested callback) {
2577        final XmppConnection connection = account.getXmppConnection();
2578        final Jid jid =
2579                connection == null
2580                        ? null
2581                        : connection.getJidForCommand(Namespace.EASY_ONBOARDING_INVITE);
2582        if (jid == null) {
2583            callback.inviteRequestFailed(
2584                    getString(R.string.server_does_not_support_easy_onboarding_invites));
2585            return;
2586        }
2587        final Iq request = new Iq(Iq.Type.SET);
2588        request.setTo(jid);
2589        final Element command = request.addChild("command", Namespace.COMMANDS);
2590        command.setAttribute("node", Namespace.EASY_ONBOARDING_INVITE);
2591        command.setAttribute("action", "execute");
2592        sendIqPacket(
2593                account,
2594                request,
2595                (response) -> {
2596                    if (response.getType() == Iq.Type.RESULT) {
2597                        final Element resultCommand =
2598                                response.findChild("command", Namespace.COMMANDS);
2599                        final Element x =
2600                                resultCommand == null
2601                                        ? null
2602                                        : resultCommand.findChild("x", Namespace.DATA);
2603                        if (x != null) {
2604                            final Data data = Data.parse(x);
2605                            final String uri = data.getValue("uri");
2606                            final String landingUrl = data.getValue("landing-url");
2607                            if (uri != null) {
2608                                final EasyOnboardingInvite invite =
2609                                        new EasyOnboardingInvite(
2610                                                jid.getDomain().toString(), uri, landingUrl);
2611                                callback.inviteRequested(invite);
2612                                return;
2613                            }
2614                        }
2615                        callback.inviteRequestFailed(getString(R.string.unable_to_parse_invite));
2616                        Log.d(Config.LOGTAG, response.toString());
2617                    } else if (response.getType() == Iq.Type.ERROR) {
2618                        callback.inviteRequestFailed(IqParser.errorMessage(response));
2619                    } else {
2620                        callback.inviteRequestFailed(getString(R.string.remote_server_timeout));
2621                    }
2622                });
2623    }
2624
2625    public void fetchBookmarks(final Account account) {
2626        final Iq iqPacket = new Iq(Iq.Type.GET);
2627        iqPacket.addExtension(new PrivateStorage()).addExtension(new Storage());
2628        final Consumer<Iq> callback =
2629                (response) -> {
2630                    if (response.getType() == Iq.Type.RESULT) {
2631                        final var privateStorage = response.getExtension(PrivateStorage.class);
2632                        if (privateStorage == null) {
2633                            return;
2634                        }
2635                        final var bookmarkStorage = privateStorage.getExtension(Storage.class);
2636                        Map<Jid, Bookmark> bookmarks =
2637                                Bookmark.parseFromStorage(bookmarkStorage, account);
2638                        processBookmarksInitial(account, bookmarks, false);
2639                    } else {
2640                        Log.d(
2641                                Config.LOGTAG,
2642                                account.getJid().asBareJid() + ": could not fetch bookmarks");
2643                    }
2644                };
2645        sendIqPacket(account, iqPacket, callback);
2646    }
2647
2648    public void fetchBookmarks2(final Account account) {
2649        final Iq retrieve = mIqGenerator.retrieveBookmarks();
2650        sendIqPacket(
2651                account,
2652                retrieve,
2653                (response) -> {
2654                    if (response.getType() == Iq.Type.RESULT) {
2655                        final var pubsub = response.getExtension(PubSub.class);
2656                        final Map<Jid, Bookmark> bookmarks =
2657                                Bookmark.parseFromPubSub(pubsub, account);
2658                        processBookmarksInitial(account, bookmarks, true);
2659                    }
2660                });
2661    }
2662
2663    public void fetchMessageDisplayedSynchronization(final Account account) {
2664        Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds");
2665        final var retrieve = mIqGenerator.retrieveMds();
2666        sendIqPacket(
2667                account,
2668                retrieve,
2669                (response) -> {
2670                    if (response.getType() != Iq.Type.RESULT) {
2671                        return;
2672                    }
2673                    final var pubsub = response.getExtension(PubSub.class);
2674                    if (pubsub == null) {
2675                        return;
2676                    }
2677                    final var items = pubsub.getItems();
2678                    if (items == null) {
2679                        return;
2680                    }
2681                    if (Namespace.MDS_DISPLAYED.equals(items.getNode())) {
2682                        for (final var item :
2683                                items.getItemMap(
2684                                                im.conversations.android.xmpp.model.mds.Displayed
2685                                                        .class)
2686                                        .entrySet()) {
2687                            processMdsItem(account, item);
2688                        }
2689                    }
2690                });
2691    }
2692
2693    public void processMdsItem(final Account account, final Map.Entry<String, Displayed> item) {
2694        final Jid jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(item.getKey()));
2695        if (jid == null) {
2696            return;
2697        }
2698        final var displayed = item.getValue();
2699        final var stanzaId = displayed.getStanzaId();
2700        final String id = stanzaId == null ? null : stanzaId.getId();
2701        final Conversation conversation = find(account, jid);
2702        if (id != null && conversation != null) {
2703            conversation.setDisplayState(id);
2704            markReadUpToStanzaId(conversation, id);
2705        }
2706    }
2707
2708    public void markReadUpToStanzaId(final Conversation conversation, final String stanzaId) {
2709        final Message message = conversation.findMessageWithServerMsgId(stanzaId);
2710        if (message == null) { // do we want to check if isRead?
2711            return;
2712        }
2713        markReadUpTo(conversation, message);
2714    }
2715
2716    public void markReadUpTo(final Conversation conversation, final Message message) {
2717        final boolean isDismissNotification = isDismissNotification(message);
2718        final var uuid = message.getUuid();
2719        Log.d(
2720                Config.LOGTAG,
2721                conversation.getAccount().getJid().asBareJid()
2722                        + ": mark "
2723                        + conversation.getJid().asBareJid()
2724                        + " as read up to "
2725                        + uuid);
2726        markRead(conversation, uuid, isDismissNotification);
2727    }
2728
2729    private static boolean isDismissNotification(final Message message) {
2730        Message next = message.next();
2731        while (next != null) {
2732            if (message.getStatus() == Message.STATUS_RECEIVED) {
2733                return false;
2734            }
2735            next = next.next();
2736        }
2737        return true;
2738    }
2739
2740    public void processBookmarksInitial(
2741            final Account account, final Map<Jid, Bookmark> bookmarks, final boolean pep) {
2742        final Set<Jid> previousBookmarks = account.getBookmarkedJids();
2743        for (final Bookmark bookmark : bookmarks.values()) {
2744            previousBookmarks.remove(bookmark.getJid().asBareJid());
2745            processModifiedBookmark(bookmark, pep);
2746        }
2747        if (pep) {
2748            processDeletedBookmarks(account, previousBookmarks);
2749        }
2750        account.setBookmarks(bookmarks);
2751    }
2752
2753    public void processDeletedBookmarks(final Account account, final Collection<Jid> bookmarks) {
2754        Log.d(
2755                Config.LOGTAG,
2756                account.getJid().asBareJid()
2757                        + ": "
2758                        + bookmarks.size()
2759                        + " bookmarks have been removed");
2760        for (final Jid bookmark : bookmarks) {
2761            processDeletedBookmark(account, bookmark);
2762        }
2763    }
2764
2765    public void processDeletedBookmark(final Account account, final Jid jid) {
2766        final Conversation conversation = find(account, jid);
2767        if (conversation == null) {
2768            return;
2769        }
2770        Log.d(
2771                Config.LOGTAG,
2772                account.getJid().asBareJid() + ": archiving MUC " + jid + " after PEP update");
2773        archiveConversation(conversation, false);
2774    }
2775
2776    private void processModifiedBookmark(final Bookmark bookmark, final boolean pep) {
2777        final Account account = bookmark.getAccount();
2778        Conversation conversation = find(bookmark);
2779        if (conversation != null) {
2780            if (conversation.getMode() != Conversation.MODE_MULTI) {
2781                return;
2782            }
2783            bookmark.setConversation(conversation);
2784            if (pep && !bookmark.autojoin()) {
2785                Log.d(
2786                        Config.LOGTAG,
2787                        account.getJid().asBareJid()
2788                                + ": archiving conference ("
2789                                + conversation.getJid()
2790                                + ") after receiving pep");
2791                archiveConversation(conversation, false);
2792            } else {
2793                final MucOptions mucOptions = conversation.getMucOptions();
2794                if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) {
2795                    final String current = mucOptions.getActualNick();
2796                    final String proposed = mucOptions.getProposedNickPure();
2797                    if (current != null && !current.equals(proposed)) {
2798                        Log.d(
2799                                Config.LOGTAG,
2800                                account.getJid().asBareJid()
2801                                        + ": proposed nick changed after bookmark push "
2802                                        + current
2803                                        + "->"
2804                                        + proposed);
2805                        joinMuc(conversation);
2806                    }
2807                } else {
2808                    checkMucRequiresRename(conversation);
2809                }
2810            }
2811        } else if (bookmark.autojoin()) {
2812            conversation =
2813                    findOrCreateConversation(account, bookmark.getFullJid(), true, true, false);
2814            bookmark.setConversation(conversation);
2815        }
2816    }
2817
2818    public void processModifiedBookmark(final Bookmark bookmark) {
2819        processModifiedBookmark(bookmark, true);
2820    }
2821
2822    public void ensureBookmarkIsAutoJoin(final Conversation conversation) {
2823        final var account = conversation.getAccount();
2824        final var existingBookmark = conversation.getBookmark();
2825        if (existingBookmark == null) {
2826            final var bookmark = new Bookmark(account, conversation.getJid().asBareJid());
2827            bookmark.setAutojoin(true);
2828            createBookmark(account, bookmark);
2829        } else {
2830            if (existingBookmark.autojoin()) {
2831                return;
2832            }
2833            existingBookmark.setAutojoin(true);
2834            createBookmark(account, existingBookmark);
2835        }
2836    }
2837
2838    public void createBookmark(final Account account, final Bookmark bookmark) {
2839        account.putBookmark(bookmark);
2840        final XmppConnection connection = account.getXmppConnection();
2841        if (connection == null) {
2842            Log.d(
2843                    Config.LOGTAG,
2844                    account.getJid().asBareJid() + ": no connection. ignoring bookmark creation");
2845        } else if (connection.getFeatures().bookmarks2()) {
2846            Log.d(
2847                    Config.LOGTAG,
2848                    account.getJid().asBareJid() + ": pushing bookmark via Bookmarks 2");
2849            final Element item = mIqGenerator.publishBookmarkItem(bookmark);
2850            pushNodeAndEnforcePublishOptions(
2851                    account,
2852                    Namespace.BOOKMARKS2,
2853                    item,
2854                    bookmark.getJid().asBareJid().toString(),
2855                    PublishOptions.persistentWhitelistAccessMaxItems());
2856        } else if (connection.getFeatures().bookmarksConversion()) {
2857            pushBookmarksPep(account);
2858        } else {
2859            pushBookmarksPrivateXml(account);
2860        }
2861    }
2862
2863    public void deleteBookmark(final Account account, final Bookmark bookmark) {
2864        if (bookmark.getJid().toString().equals("discuss@conference.soprani.ca")) {
2865            getPreferences().edit().putBoolean("cheogram_sopranica_bookmark_deleted", true).apply();
2866        }
2867        account.removeBookmark(bookmark);
2868        final XmppConnection connection = account.getXmppConnection();
2869        if (connection == null) return;
2870
2871        if (connection.getFeatures().bookmarks2()) {
2872            final Iq request =
2873                    mIqGenerator.deleteItem(
2874                            Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toString());
2875            Log.d(
2876                    Config.LOGTAG,
2877                    account.getJid().asBareJid() + ": removing bookmark via Bookmarks 2");
2878            sendIqPacket(
2879                    account,
2880                    request,
2881                    (response) -> {
2882                        if (response.getType() == Iq.Type.ERROR) {
2883                            Log.d(
2884                                    Config.LOGTAG,
2885                                    account.getJid().asBareJid()
2886                                            + ": unable to delete bookmark "
2887                                            + response.getErrorCondition());
2888                        }
2889                    });
2890        } else if (connection.getFeatures().bookmarksConversion()) {
2891            pushBookmarksPep(account);
2892        } else {
2893            pushBookmarksPrivateXml(account);
2894        }
2895    }
2896
2897    private void pushBookmarksPrivateXml(Account account) {
2898        if (!account.areBookmarksLoaded()) return;
2899
2900        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml");
2901        final Iq iqPacket = new Iq(Iq.Type.SET);
2902        Element query = iqPacket.query("jabber:iq:private");
2903        Element storage = query.addChild("storage", "storage:bookmarks");
2904        for (final Bookmark bookmark : account.getBookmarks()) {
2905            storage.addChild(bookmark);
2906        }
2907        sendIqPacket(account, iqPacket, mDefaultIqHandler);
2908    }
2909
2910    private void pushBookmarksPep(Account account) {
2911        if (!account.areBookmarksLoaded()) return;
2912
2913        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via pep");
2914        final Element storage = new Element("storage", "storage:bookmarks");
2915        for (final Bookmark bookmark : account.getBookmarks()) {
2916            storage.addChild(bookmark);
2917        }
2918        pushNodeAndEnforcePublishOptions(
2919                account,
2920                Namespace.BOOKMARKS,
2921                storage,
2922                "current",
2923                PublishOptions.persistentWhitelistAccess());
2924    }
2925
2926    private void pushNodeAndEnforcePublishOptions(
2927            final Account account,
2928            final String node,
2929            final Element element,
2930            final String id,
2931            final Bundle options) {
2932        pushNodeAndEnforcePublishOptions(account, node, element, id, options, true);
2933    }
2934
2935    private void pushNodeAndEnforcePublishOptions(
2936            final Account account,
2937            final String node,
2938            final Element element,
2939            final String id,
2940            final Bundle options,
2941            final boolean retry) {
2942        final Iq packet = mIqGenerator.publishElement(node, element, id, options);
2943        sendIqPacket(
2944                account,
2945                packet,
2946                (response) -> {
2947                    if (response.getType() == Iq.Type.RESULT) {
2948                        return;
2949                    }
2950                    if (retry && PublishOptions.preconditionNotMet(response)) {
2951                        pushNodeConfiguration(
2952                                account,
2953                                node,
2954                                options,
2955                                new OnConfigurationPushed() {
2956                                    @Override
2957                                    public void onPushSucceeded() {
2958                                        pushNodeAndEnforcePublishOptions(
2959                                                account, node, element, id, options, false);
2960                                    }
2961
2962                                    @Override
2963                                    public void onPushFailed() {
2964                                        Log.d(
2965                                                Config.LOGTAG,
2966                                                account.getJid().asBareJid()
2967                                                        + ": unable to push node configuration ("
2968                                                        + node
2969                                                        + ")");
2970                                    }
2971                                });
2972                    } else {
2973                        Log.d(
2974                                Config.LOGTAG,
2975                                account.getJid().asBareJid()
2976                                        + ": error publishing "
2977                                        + node
2978                                        + " (retry="
2979                                        + retry
2980                                        + ") "
2981                                        + response);
2982                    }
2983                });
2984    }
2985
2986    private void restoreFromDatabase() {
2987        synchronized (this.conversations) {
2988            final Map<String, Account> accountLookupTable =
2989                    ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
2990            Log.d(Config.LOGTAG, "restoring conversations...");
2991            final long startTimeConversationsRestore = SystemClock.elapsedRealtime();
2992            this.conversations.addAll(
2993                    databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
2994            for (Iterator<Conversation> iterator = conversations.listIterator();
2995                    iterator.hasNext(); ) {
2996                Conversation conversation = iterator.next();
2997                Account account = accountLookupTable.get(conversation.getAccountUuid());
2998                if (account != null) {
2999                    conversation.setAccount(account);
3000                } else {
3001                    Log.e(Config.LOGTAG, "unable to restore Conversations with " + conversation.getJid());
3002                    conversations.remove(conversation);
3003                }
3004            }
3005            long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore;
3006            Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms");
3007            Runnable runnable = () -> {
3008                if (DatabaseBackend.requiresMessageIndexRebuild()) {
3009                    DatabaseBackend.getInstance(this).rebuildMessagesIndex();
3010                }
3011                mutedMucUsers = databaseBackend.loadMutedMucUsers();
3012                final long deletionDate = getAutomaticMessageDeletionDate();
3013                mLastExpiryRun.set(SystemClock.elapsedRealtime());
3014                if (deletionDate > 0) {
3015                    Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate));
3016                    databaseBackend.expireOldMessages(deletionDate);
3017                }
3018                Log.d(Config.LOGTAG, "restoring roster...");
3019                for (final Account account : accounts) {
3020                    databaseBackend.readRoster(account.getRoster());
3021                    account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage
3022                }
3023                getDrawableCache().evictAll();
3024                loadPhoneContacts();
3025                Log.d(Config.LOGTAG, "restoring messages...");
3026                final long startMessageRestore = SystemClock.elapsedRealtime();
3027                final Conversation quickLoad = QuickLoader.get(this.conversations);
3028                if (quickLoad != null) {
3029                    restoreMessages(quickLoad);
3030                    updateConversationUi();
3031                    final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
3032                    Log.d(Config.LOGTAG, "quickly restored " + quickLoad.getName() + " after " + diffMessageRestore + "ms");
3033                }
3034                for (Conversation conversation : this.conversations) {
3035                    if (quickLoad != conversation) {
3036                        restoreMessages(conversation);
3037                    }
3038                }
3039                mNotificationService.finishBacklog();
3040                restoredFromDatabaseLatch.countDown();
3041                final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
3042                Log.d(Config.LOGTAG, "finished restoring messages in " + diffMessageRestore + "ms");
3043                updateConversationUi();
3044            };
3045            mDatabaseReaderExecutor.execute(runnable); //will contain one write command (expiry) but that's fine
3046        }
3047    }
3048
3049    private void restoreMessages(Conversation conversation) {
3050        conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
3051        conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
3052        conversation.findMessagesAndCallsToNotify(mNotificationService::pushFromBacklog);
3053    }
3054
3055    public void loadPhoneContacts() {
3056        mContactMergerExecutor.execute(
3057                () -> {
3058                    final Map<Jid, JabberIdContact> contacts = JabberIdContact.load(this);
3059                    Log.d(Config.LOGTAG, "start merging phone contacts with roster");
3060                    for (final Account account : accounts) {
3061                        final List<Contact> withSystemAccounts =
3062                                account.getRoster().getWithSystemAccounts(JabberIdContact.class);
3063                        for (final JabberIdContact jidContact : contacts.values()) {
3064                            final Contact contact =
3065                                    account.getRoster().getContact(jidContact.getJid());
3066                            boolean needsCacheClean = contact.setPhoneContact(jidContact);
3067                            if (needsCacheClean) {
3068                                getAvatarService().clear(contact);
3069                            }
3070                            withSystemAccounts.remove(contact);
3071                        }
3072                        for (final Contact contact : withSystemAccounts) {
3073                            boolean needsCacheClean =
3074                                    contact.unsetPhoneContact(JabberIdContact.class);
3075                            if (needsCacheClean) {
3076                                getAvatarService().clear(contact);
3077                            }
3078                        }
3079                    }
3080                    Log.d(Config.LOGTAG, "finished merging phone contacts");
3081                    mShortcutService.refresh(
3082                            mInitialAddressbookSyncCompleted.compareAndSet(false, true));
3083                    updateRosterUi(UpdateRosterReason.PUSH);
3084                    mQuickConversationsService.considerSync();
3085                });
3086    }
3087
3088    public void syncRoster(final Account account) {
3089        mRosterSyncTaskManager.execute(account, () -> {
3090            unregisterPhoneAccounts(account);
3091            databaseBackend.writeRoster(account.getRoster());
3092            try { Thread.sleep(500); } catch (InterruptedException e) { }
3093        });
3094    }
3095
3096    public List<Conversation> getConversations() {
3097        return this.conversations;
3098    }
3099
3100    private void markFileDeleted(final File file) {
3101        synchronized (FILENAMES_TO_IGNORE_DELETION) {
3102            if (FILENAMES_TO_IGNORE_DELETION.remove(file.getAbsolutePath())) {
3103                Log.d(Config.LOGTAG, "ignored deletion of " + file.getAbsolutePath());
3104                return;
3105            }
3106        }
3107        final boolean isInternalFile = fileBackend.isInternalFile(file);
3108        final List<String> uuids = databaseBackend.markFileAsDeleted(file, isInternalFile);
3109        Log.d(
3110                Config.LOGTAG,
3111                "deleted file "
3112                        + file.getAbsolutePath()
3113                        + " internal="
3114                        + isInternalFile
3115                        + ", database hits="
3116                        + uuids.size());
3117        markUuidsAsDeletedFiles(uuids);
3118    }
3119
3120    private void markUuidsAsDeletedFiles(List<String> uuids) {
3121        boolean deleted = false;
3122        for (Conversation conversation : getConversations()) {
3123            deleted |= conversation.markAsDeleted(uuids);
3124        }
3125        for (final String uuid : uuids) {
3126            evictPreview(uuid);
3127        }
3128        if (deleted) {
3129            updateConversationUi();
3130        }
3131    }
3132
3133    private void markChangedFiles(List<DatabaseBackend.FilePathInfo> infos) {
3134        boolean changed = false;
3135        for (Conversation conversation : getConversations()) {
3136            changed |= conversation.markAsChanged(infos);
3137        }
3138        if (changed) {
3139            updateConversationUi();
3140        }
3141    }
3142
3143    public void populateWithOrderedConversations(final List<Conversation> list) {
3144        populateWithOrderedConversations(list, true, true);
3145    }
3146
3147    public void populateWithOrderedConversations(
3148            final List<Conversation> list, final boolean includeNoFileUpload) {
3149        populateWithOrderedConversations(list, includeNoFileUpload, true);
3150    }
3151
3152    public void populateWithOrderedConversations(
3153            final List<Conversation> list, final boolean includeNoFileUpload, final boolean sort) {
3154        final List<String> orderedUuids;
3155        if (sort) {
3156            orderedUuids = null;
3157        } else {
3158            orderedUuids = new ArrayList<>();
3159            for (Conversation conversation : list) {
3160                orderedUuids.add(conversation.getUuid());
3161            }
3162        }
3163        list.clear();
3164        if (includeNoFileUpload) {
3165            list.addAll(getConversations());
3166        } else {
3167            for (Conversation conversation : getConversations()) {
3168                if (conversation.getMode() == Conversation.MODE_SINGLE
3169                        || (conversation.getAccount().httpUploadAvailable()
3170                                && conversation.getMucOptions().participating())) {
3171                    list.add(conversation);
3172                }
3173            }
3174        }
3175        try {
3176            if (orderedUuids != null) {
3177                Collections.sort(
3178                        list,
3179                        (a, b) -> {
3180                            final int indexA = orderedUuids.indexOf(a.getUuid());
3181                            final int indexB = orderedUuids.indexOf(b.getUuid());
3182                            if (indexA == -1 || indexB == -1 || indexA == indexB) {
3183                                return a.compareTo(b);
3184                            }
3185                            return indexA - indexB;
3186                        });
3187            } else {
3188                Collections.sort(list);
3189            }
3190        } catch (IllegalArgumentException e) {
3191            // ignore
3192        }
3193    }
3194
3195    public void loadMoreMessages(
3196            final Conversation conversation,
3197            final long timestamp,
3198            final OnMoreMessagesLoaded callback) {
3199        if (XmppConnectionService.this
3200                .getMessageArchiveService()
3201                .queryInProgress(conversation, callback)) {
3202            return;
3203        } else if (timestamp == 0) {
3204            return;
3205        }
3206        Log.d(
3207                Config.LOGTAG,
3208                "load more messages for "
3209                        + conversation.getName()
3210                        + " prior to "
3211                        + MessageGenerator.getTimestamp(timestamp));
3212        final Runnable runnable =
3213                () -> {
3214                    final Account account = conversation.getAccount();
3215                    List<Message> messages =
3216                            databaseBackend.getMessages(conversation, 50, timestamp);
3217                    if (messages.size() > 0) {
3218                        conversation.addAll(0, messages);
3219                        callback.onMoreMessagesLoaded(messages.size(), conversation);
3220                    } else if (conversation.hasMessagesLeftOnServer()
3221                            && account.isOnlineAndConnected()
3222                            && conversation.getLastClearHistory().getTimestamp() == 0) {
3223                        final boolean mamAvailable;
3224                        if (conversation.getMode() == Conversation.MODE_SINGLE) {
3225                            mamAvailable =
3226                                    account.getXmppConnection().getFeatures().mam()
3227                                            && !conversation.getContact().isBlocked();
3228                        } else {
3229                            mamAvailable = conversation.getMucOptions().mamSupport();
3230                        }
3231                        if (mamAvailable) {
3232                            MessageArchiveService.Query query =
3233                                    getMessageArchiveService()
3234                                            .query(
3235                                                    conversation,
3236                                                    new MamReference(0),
3237                                                    timestamp,
3238                                                    false);
3239                            if (query != null) {
3240                                query.setCallback(callback);
3241                                callback.informUser(R.string.fetching_history_from_server);
3242                            } else {
3243                                callback.informUser(R.string.not_fetching_history_retention_period);
3244                            }
3245                        }
3246                    }
3247                };
3248        mDatabaseReaderExecutor.execute(runnable);
3249    }
3250
3251    public List<Account> getAccounts() {
3252        return this.accounts;
3253    }
3254
3255    /**
3256     * This will find all conferences with the contact as member and also the conference that is the
3257     * contact (that 'fake' contact is used to store the avatar)
3258     */
3259    public List<Conversation> findAllConferencesWith(Contact contact) {
3260        final ArrayList<Conversation> results = new ArrayList<>();
3261        for (final Conversation c : conversations) {
3262            if (c.getMode() != Conversation.MODE_MULTI) {
3263                continue;
3264            }
3265            final MucOptions mucOptions = c.getMucOptions();
3266            if (c.getJid().asBareJid().equals(contact.getJid().asBareJid())
3267                    || (mucOptions != null && mucOptions.isContactInRoom(contact))) {
3268                results.add(c);
3269            }
3270        }
3271        return results;
3272    }
3273
3274    public Conversation find(final Contact contact) {
3275        for (final Conversation conversation : this.conversations) {
3276            if (conversation.getContact() == contact) {
3277                return conversation;
3278            }
3279        }
3280        return null;
3281    }
3282
3283    public Conversation find(
3284            final Iterable<Conversation> haystack, final Account account, final Jid jid) {
3285        if (jid == null) {
3286            return null;
3287        }
3288        for (final Conversation conversation : haystack) {
3289            if ((account == null || conversation.getAccount() == account)
3290                    && (conversation.getJid().asBareJid().equals(jid.asBareJid()))) {
3291                return conversation;
3292            }
3293        }
3294        return null;
3295    }
3296
3297    public boolean isConversationsListEmpty(final Conversation ignore) {
3298        synchronized (this.conversations) {
3299            final int size = this.conversations.size();
3300            return size == 0 || size == 1 && this.conversations.get(0) == ignore;
3301        }
3302    }
3303
3304    public boolean isConversationStillOpen(final Conversation conversation) {
3305        synchronized (this.conversations) {
3306            for (Conversation current : this.conversations) {
3307                if (current == conversation) {
3308                    return true;
3309                }
3310            }
3311        }
3312        return false;
3313    }
3314
3315    public void maybeRegisterWithMuc(Conversation c, String nickArg) {
3316        final var nick = nickArg == null ? c.getMucOptions().getSelf().getFullJid().getResource() : nickArg;
3317        final var register = new Iq(Iq.Type.GET);
3318        register.query(Namespace.REGISTER);
3319        register.setTo(c.getJid().asBareJid());
3320        sendIqPacket(c.getAccount(), register, (response) -> {
3321            if (response.getType() == Iq.Type.RESULT) {
3322                final Element query = response.query(Namespace.REGISTER);
3323                String username = query.findChildContent("username", Namespace.REGISTER);
3324                if (username == null) username = query.findChildContent("nick", Namespace.REGISTER);
3325                if (username != null && username.equals(nick)) {
3326                    // Already registered with this nick, done
3327                    Log.d(Config.LOGTAG, "Already registered with " + c.getJid().asBareJid() + " as " + username);
3328                    return;
3329                }
3330                Data form = Data.parse(query.findChild("x", Namespace.DATA));
3331                if (form != null) {
3332                    final var field = form.getFieldByName("muc#register_roomnick");
3333                    if (field != null && nick.equals(field.getValue())) {
3334                        Log.d(Config.LOGTAG, "Already registered with " + c.getJid().asBareJid() + " as " + field.getValue());
3335                        return;
3336                    }
3337                }
3338                if (form == null || !"form".equals(form.getFormType()) || !form.getFields().stream().anyMatch(f -> f.isRequired() && !"muc#register_roomnick".equals(f.getFieldName()))) {
3339                    // No form, result form, or no required fields other than nickname, let's just send nickname
3340                    if (form == null || !"form".equals(form.getFormType())) {
3341                        form = new Data();
3342                        form.put("FORM_TYPE", "http://jabber.org/protocol/muc#register");
3343                    }
3344                    form.put("muc#register_roomnick", nick);
3345                    form.submit();
3346                    final var finish = new Iq(Iq.Type.SET);
3347                    finish.query(Namespace.REGISTER).addChild(form);
3348                    finish.setTo(c.getJid().asBareJid());
3349                    sendIqPacket(c.getAccount(), finish, (response2) -> {
3350                        if (response.getType() == Iq.Type.RESULT) {
3351                            Log.w(Config.LOGTAG, "Success registering with channel " + c.getJid().asBareJid() + "/" + nick);
3352                        } else {
3353                            Log.w(Config.LOGTAG, "Error registering with channel: " + response2);
3354                        }
3355                    });
3356                } else {
3357                    // TODO: offer registration form to user
3358                    Log.d(Config.LOGTAG, "Complex registration form for " + c.getJid().asBareJid() + ": " + response);
3359                }
3360            } else {
3361                // We said maybe. Guess not
3362                Log.d(Config.LOGTAG, "Could not register with " + c.getJid().asBareJid() + ": " + response);
3363            }
3364        });
3365    }
3366
3367    public void deregisterWithMuc(Conversation c) {
3368        final Iq register = new Iq(Iq.Type.GET);
3369        register.query(Namespace.REGISTER).addChild("remove");
3370        register.setTo(c.getJid().asBareJid());
3371        sendIqPacket(c.getAccount(), register, (response) -> {
3372            if (response.getType() == Iq.Type.RESULT) {
3373                Log.d(Config.LOGTAG, "deregistered with " + c.getJid().asBareJid());
3374            } else {
3375                Log.w(Config.LOGTAG, "Could not deregister with " + c.getJid().asBareJid() + ": " + response);
3376            }
3377        });
3378    }
3379
3380    public Conversation findOrCreateConversation(
3381            Account account, Jid jid, boolean muc, final boolean async) {
3382        return this.findOrCreateConversation(account, jid, muc, false, async);
3383    }
3384
3385    public Conversation findOrCreateConversation(
3386            final Account account,
3387            final Jid jid,
3388            final boolean muc,
3389            final boolean joinAfterCreate,
3390            final boolean async) {
3391        return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, null, async, null);
3392    }
3393
3394    public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final MessageArchiveService.Query query, final boolean async) {
3395        return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, query, async, null);
3396    }
3397
3398    public Conversation findOrCreateConversation(
3399            final Account account,
3400            final Jid jid,
3401            final boolean muc,
3402            final boolean joinAfterCreate,
3403            final MessageArchiveService.Query query,
3404            final boolean async,
3405            final String password) {
3406        synchronized (this.conversations) {
3407            final var cached = find(account, jid);
3408            if (cached != null) {
3409                return cached;
3410            }
3411            final var existing = databaseBackend.findConversation(account, jid);
3412            final Conversation conversation;
3413            final boolean loadMessagesFromDb;
3414            if (existing != null) {
3415                conversation = existing;
3416                if (password != null) conversation.getMucOptions().setPassword(password);
3417                loadMessagesFromDb = restoreFromArchive(conversation, jid, muc);
3418            } else {
3419                String conversationName;
3420                final Contact contact = account.getRoster().getContact(jid);
3421                if (contact != null) {
3422                    conversationName = contact.getDisplayName();
3423                } else {
3424                    conversationName = jid.getLocal();
3425                }
3426                if (muc) {
3427                    conversation =
3428                            new Conversation(
3429                                    conversationName, account, jid, Conversation.MODE_MULTI);
3430                } else {
3431                    conversation =
3432                            new Conversation(
3433                                    conversationName,
3434                                    account,
3435                                    jid.asBareJid(),
3436                                    Conversation.MODE_SINGLE);
3437                }
3438                if (password != null) conversation.getMucOptions().setPassword(password);
3439                this.databaseBackend.createConversation(conversation);
3440                loadMessagesFromDb = false;
3441            }
3442            if (async) {
3443                mDatabaseReaderExecutor.execute(
3444                        () ->
3445                                postProcessConversation(
3446                                        conversation, loadMessagesFromDb, joinAfterCreate, query));
3447            } else {
3448                postProcessConversation(conversation, loadMessagesFromDb, joinAfterCreate, query);
3449            }
3450            this.conversations.add(conversation);
3451            updateConversationUi();
3452            return conversation;
3453        }
3454    }
3455
3456    public Conversation findConversationByUuidReliable(final String uuid) {
3457        final var cached = findConversationByUuid(uuid);
3458        if (cached != null) {
3459            return cached;
3460        }
3461        final var existing = databaseBackend.findConversation(uuid);
3462        if (existing == null) {
3463            return null;
3464        }
3465        Log.d(Config.LOGTAG, "restoring conversation with " + existing.getJid() + " from DB");
3466        final Map<String, Account> accounts =
3467                ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
3468        final var account = accounts.get(existing.getAccountUuid());
3469        if (account == null) {
3470            Log.d(Config.LOGTAG, "could not find account " + existing.getAccountUuid());
3471            return null;
3472        }
3473        existing.setAccount(account);
3474        final var loadMessagesFromDb = restoreFromArchive(existing);
3475        mDatabaseReaderExecutor.execute(
3476                () ->
3477                        postProcessConversation(
3478                                existing,
3479                                loadMessagesFromDb,
3480                                existing.getMode() == Conversational.MODE_MULTI,
3481                                null));
3482        this.conversations.add(existing);
3483        if (existing.getMode() == Conversational.MODE_MULTI) {
3484            ensureBookmarkIsAutoJoin(existing);
3485        }
3486        updateConversationUi();
3487        return existing;
3488    }
3489
3490    private boolean restoreFromArchive(
3491            final Conversation conversation, final Jid jid, final boolean muc) {
3492        if (muc) {
3493            conversation.setMode(Conversation.MODE_MULTI);
3494            conversation.setContactJid(jid);
3495        } else {
3496            conversation.setMode(Conversation.MODE_SINGLE);
3497            conversation.setContactJid(jid.asBareJid());
3498        }
3499        return restoreFromArchive(conversation);
3500    }
3501
3502    private boolean restoreFromArchive(final Conversation conversation) {
3503        conversation.setStatus(Conversation.STATUS_AVAILABLE);
3504        databaseBackend.updateConversation(conversation);
3505        return conversation.messagesLoaded.compareAndSet(true, false);
3506    }
3507
3508    private void postProcessConversation(
3509            final Conversation c,
3510            final boolean loadMessagesFromDb,
3511            final boolean joinAfterCreate,
3512            final MessageArchiveService.Query query) {
3513        final var singleMode = c.getMode() == Conversational.MODE_SINGLE;
3514        final var account = c.getAccount();
3515        if (loadMessagesFromDb) {
3516            c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE));
3517            updateConversationUi();
3518            c.messagesLoaded.set(true);
3519        }
3520        if (account.getXmppConnection() != null
3521                && !c.getContact().isBlocked()
3522                && account.getXmppConnection().getFeatures().mam()
3523                && singleMode) {
3524            if (query == null) {
3525                mMessageArchiveService.query(c);
3526            } else {
3527                if (query.getConversation() == null) {
3528                    mMessageArchiveService.query(c, query.getStart(), query.isCatchup());
3529                }
3530            }
3531        }
3532        if (joinAfterCreate) {
3533            joinMuc(c);
3534        }
3535    }
3536
3537    public void archiveConversation(Conversation conversation) {
3538        archiveConversation(conversation, true);
3539    }
3540
3541    private void archiveConversation(
3542            Conversation conversation, final boolean maySynchronizeWithBookmarks) {
3543        if (isOnboarding()) return;
3544
3545        getNotificationService().clear(conversation);
3546        conversation.setStatus(Conversation.STATUS_ARCHIVED);
3547        conversation.setNextMessage(null);
3548        synchronized (this.conversations) {
3549            getMessageArchiveService().kill(conversation);
3550            if (conversation.getMode() == Conversation.MODE_MULTI) {
3551                if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
3552                    final Bookmark bookmark = conversation.getBookmark();
3553                    if (maySynchronizeWithBookmarks && bookmark != null) {
3554                        if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) {
3555                            Account account = bookmark.getAccount();
3556                            bookmark.setConversation(null);
3557                            deleteBookmark(account, bookmark);
3558                        } else if (bookmark.autojoin()) {
3559                            bookmark.setAutojoin(false);
3560                            createBookmark(bookmark.getAccount(), bookmark);
3561                        }
3562                    }
3563                }
3564                deregisterWithMuc(conversation);
3565                leaveMuc(conversation);
3566            } else {
3567                if (conversation
3568                        .getContact()
3569                        .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
3570                    stopPresenceUpdatesTo(conversation.getContact());
3571                }
3572            }
3573            updateConversation(conversation);
3574            this.conversations.remove(conversation);
3575            updateConversationUi();
3576        }
3577    }
3578
3579    public void stopPresenceUpdatesTo(Contact contact) {
3580        Log.d(Config.LOGTAG, "Canceling presence request from " + contact.getJid().toString());
3581        sendPresencePacket(contact.getAccount(), mPresenceGenerator.stopPresenceUpdatesTo(contact));
3582        contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
3583    }
3584
3585    public void createAccount(final Account account) {
3586        account.initAccountServices(this);
3587        databaseBackend.createAccount(account);
3588        if (CallIntegration.hasSystemFeature(this)) {
3589            CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
3590        }
3591        this.accounts.add(account);
3592        this.reconnectAccountInBackground(account);
3593        updateAccountUi();
3594        syncEnabledAccountSetting();
3595        toggleForegroundService();
3596    }
3597
3598    private void syncEnabledAccountSetting() {
3599        final boolean hasEnabledAccounts = hasEnabledAccounts();
3600        getPreferences()
3601                .edit()
3602                .putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts)
3603                .apply();
3604        toggleSetProfilePictureActivity(hasEnabledAccounts);
3605    }
3606
3607    private void toggleSetProfilePictureActivity(final boolean enabled) {
3608        try {
3609            final ComponentName name =
3610                    new ComponentName(this, ChooseAccountForProfilePictureActivity.class);
3611            final int targetState =
3612                    enabled
3613                            ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
3614                            : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
3615            getPackageManager()
3616                    .setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP);
3617        } catch (IllegalStateException e) {
3618            Log.d(Config.LOGTAG, "unable to toggle profile picture activity");
3619        }
3620    }
3621
3622    public boolean reconfigurePushDistributor() {
3623        return this.unifiedPushBroker.reconfigurePushDistributor();
3624    }
3625
3626    private Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints(
3627            final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger) {
3628        return this.unifiedPushBroker.renewUnifiedPushEndpoints(pushTargetMessenger);
3629    }
3630
3631    public Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints() {
3632        return this.unifiedPushBroker.renewUnifiedPushEndpoints(null);
3633    }
3634
3635    public UnifiedPushBroker getUnifiedPushBroker() {
3636        return this.unifiedPushBroker;
3637    }
3638
3639    private void provisionAccount(final String address, final String password) {
3640        final Jid jid = Jid.of(address);
3641        final Account account = new Account(jid, password);
3642        account.setOption(Account.OPTION_DISABLED, true);
3643        Log.d(Config.LOGTAG, jid.asBareJid().toString() + ": provisioning account");
3644        createAccount(account);
3645    }
3646
3647    public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
3648        new Thread(
3649                        () -> {
3650                            try {
3651                                final X509Certificate[] chain =
3652                                        KeyChain.getCertificateChain(this, alias);
3653                                final X509Certificate cert =
3654                                        chain != null && chain.length > 0 ? chain[0] : null;
3655                                if (cert == null) {
3656                                    callback.informUser(R.string.unable_to_parse_certificate);
3657                                    return;
3658                                }
3659                                Pair<Jid, String> info = CryptoHelper.extractJidAndName(cert);
3660                                if (info == null) {
3661                                    callback.informUser(R.string.certificate_does_not_contain_jid);
3662                                    return;
3663                                }
3664                                if (findAccountByJid(info.first) == null) {
3665                                    final Account account = new Account(info.first, "");
3666                                    account.setPrivateKeyAlias(alias);
3667                                    account.setOption(Account.OPTION_DISABLED, true);
3668                                    account.setOption(Account.OPTION_FIXED_USERNAME, true);
3669                                    account.setDisplayName(info.second);
3670                                    createAccount(account);
3671                                    callback.onAccountCreated(account);
3672                                    if (Config.X509_VERIFICATION) {
3673                                        try {
3674                                            getMemorizingTrustManager()
3675                                                    .getNonInteractive(account.getServer(), null, 0, null)
3676                                                    .checkClientTrusted(chain, "RSA");
3677                                        } catch (CertificateException e) {
3678                                            callback.informUser(
3679                                                    R.string.certificate_chain_is_not_trusted);
3680                                        }
3681                                    }
3682                                } else {
3683                                    callback.informUser(R.string.account_already_exists);
3684                                }
3685                            } catch (Exception e) {
3686                                callback.informUser(R.string.unable_to_parse_certificate);
3687                            }
3688                        })
3689                .start();
3690    }
3691
3692    public void updateKeyInAccount(final Account account, final String alias) {
3693        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": update key in account " + alias);
3694        try {
3695            X509Certificate[] chain =
3696                    KeyChain.getCertificateChain(XmppConnectionService.this, alias);
3697            Log.d(Config.LOGTAG, account.getJid().asBareJid() + " loaded certificate chain");
3698            Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
3699            if (info == null) {
3700                showErrorToastInUi(R.string.certificate_does_not_contain_jid);
3701                return;
3702            }
3703            if (account.getJid().asBareJid().equals(info.first)) {
3704                account.setPrivateKeyAlias(alias);
3705                account.setDisplayName(info.second);
3706                databaseBackend.updateAccount(account);
3707                if (Config.X509_VERIFICATION) {
3708                    try {
3709                        getMemorizingTrustManager()
3710                                .getNonInteractive()
3711                                .checkClientTrusted(chain, "RSA");
3712                    } catch (CertificateException e) {
3713                        showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
3714                    }
3715                    account.getAxolotlService().regenerateKeys(true);
3716                }
3717            } else {
3718                showErrorToastInUi(R.string.jid_does_not_match_certificate);
3719            }
3720        } catch (Exception e) {
3721            e.printStackTrace();
3722        }
3723    }
3724
3725    public boolean updateAccount(final Account account) {
3726        if (databaseBackend.updateAccount(account)) {
3727            Integer color = account.getColorToSave();
3728            if (color == null) {
3729                getPreferences().edit().remove("account_color:" + account.getUuid()).commit();
3730            } else {
3731                getPreferences().edit().putInt("account_color:" + account.getUuid(), color.intValue()).commit();
3732            }
3733            account.setShowErrorNotification(true);
3734            this.statusListener.onStatusChanged(account);
3735            databaseBackend.updateAccount(account);
3736            reconnectAccountInBackground(account);
3737            updateAccountUi();
3738            getNotificationService().updateErrorNotification();
3739            toggleForegroundService();
3740            syncEnabledAccountSetting();
3741            mChannelDiscoveryService.cleanCache();
3742            if (CallIntegration.hasSystemFeature(this)) {
3743                CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
3744            }
3745            return true;
3746        } else {
3747            return false;
3748        }
3749    }
3750
3751    public void updateAccountPasswordOnServer(
3752            final Account account,
3753            final String newPassword,
3754            final OnAccountPasswordChanged callback) {
3755        final Iq iq = getIqGenerator().generateSetPassword(account, newPassword);
3756        sendIqPacket(
3757                account,
3758                iq,
3759                (packet) -> {
3760                    if (packet.getType() == Iq.Type.RESULT) {
3761                        account.setPassword(newPassword);
3762                        account.setOption(Account.OPTION_MAGIC_CREATE, false);
3763                        databaseBackend.updateAccount(account);
3764                        callback.onPasswordChangeSucceeded();
3765                    } else {
3766                        callback.onPasswordChangeFailed();
3767                    }
3768                });
3769    }
3770
3771    public void unregisterAccount(final Account account, final Consumer<Boolean> callback) {
3772        final Iq iqPacket = new Iq(Iq.Type.SET);
3773        final Element query = iqPacket.addChild("query", Namespace.REGISTER);
3774        query.addChild("remove");
3775        sendIqPacket(
3776                account,
3777                iqPacket,
3778                (response) -> {
3779                    if (response.getType() == Iq.Type.RESULT) {
3780                        deleteAccount(account);
3781                        callback.accept(true);
3782                    } else {
3783                        callback.accept(false);
3784                    }
3785                });
3786    }
3787
3788    public void deleteAccount(final Account account) {
3789        getPreferences().edit().remove("onboarding_continued").commit();
3790        final boolean connected = account.getStatus() == Account.State.ONLINE;
3791        synchronized (this.conversations) {
3792            if (connected) {
3793                account.getAxolotlService().deleteOmemoIdentity();
3794            }
3795            for (final Conversation conversation : conversations) {
3796                if (conversation.getAccount() == account) {
3797                    if (conversation.getMode() == Conversation.MODE_MULTI) {
3798                        if (connected) {
3799                            leaveMuc(conversation);
3800                        }
3801                    }
3802                    conversations.remove(conversation);
3803                    mNotificationService.clear(conversation);
3804                }
3805            }
3806            new Thread(() -> {
3807                for (final Contact contact : account.getRoster().getContacts()) {
3808                    contact.unregisterAsPhoneAccount(this);
3809                }
3810            }).start();
3811            if (account.getXmppConnection() != null) {
3812                new Thread(() -> disconnect(account, !connected)).start();
3813            }
3814            final Runnable runnable =
3815                    () -> {
3816                        if (!databaseBackend.deleteAccount(account)) {
3817                            Log.d(
3818                                    Config.LOGTAG,
3819                                    account.getJid().asBareJid() + ": unable to delete account");
3820                        }
3821                    };
3822            mDatabaseWriterExecutor.execute(runnable);
3823            this.accounts.remove(account);
3824            if (CallIntegration.hasSystemFeature(this)) {
3825                CallIntegrationConnectionService.unregisterPhoneAccount(this, account);
3826            }
3827            this.mRosterSyncTaskManager.clear(account);
3828            updateAccountUi();
3829            mNotificationService.updateErrorNotification();
3830            syncEnabledAccountSetting();
3831            toggleForegroundService();
3832        }
3833    }
3834
3835    public void setOnConversationListChangedListener(OnConversationUpdate listener) {
3836        final boolean remainingListeners;
3837        synchronized (LISTENER_LOCK) {
3838            remainingListeners = checkListeners();
3839            if (!this.mOnConversationUpdates.add(listener)) {
3840                Log.w(
3841                        Config.LOGTAG,
3842                        listener.getClass().getName()
3843                                + " is already registered as ConversationListChangedListener");
3844            }
3845            this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
3846        }
3847        if (remainingListeners) {
3848            switchToForeground();
3849        }
3850    }
3851
3852    public void removeOnConversationListChangedListener(OnConversationUpdate listener) {
3853        final boolean remainingListeners;
3854        synchronized (LISTENER_LOCK) {
3855            this.mOnConversationUpdates.remove(listener);
3856            this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
3857            remainingListeners = checkListeners();
3858        }
3859        if (remainingListeners) {
3860            switchToBackground();
3861        }
3862    }
3863
3864    public void setOnShowErrorToastListener(OnShowErrorToast listener) {
3865        final boolean remainingListeners;
3866        synchronized (LISTENER_LOCK) {
3867            remainingListeners = checkListeners();
3868            if (!this.mOnShowErrorToasts.add(listener)) {
3869                Log.w(
3870                        Config.LOGTAG,
3871                        listener.getClass().getName()
3872                                + " is already registered as OnShowErrorToastListener");
3873            }
3874        }
3875        if (remainingListeners) {
3876            switchToForeground();
3877        }
3878    }
3879
3880    public void removeOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
3881        final boolean remainingListeners;
3882        synchronized (LISTENER_LOCK) {
3883            this.mOnShowErrorToasts.remove(onShowErrorToast);
3884            remainingListeners = checkListeners();
3885        }
3886        if (remainingListeners) {
3887            switchToBackground();
3888        }
3889    }
3890
3891    public void setOnAccountListChangedListener(OnAccountUpdate listener) {
3892        final boolean remainingListeners;
3893        synchronized (LISTENER_LOCK) {
3894            remainingListeners = checkListeners();
3895            if (!this.mOnAccountUpdates.add(listener)) {
3896                Log.w(
3897                        Config.LOGTAG,
3898                        listener.getClass().getName()
3899                                + " is already registered as OnAccountListChangedtListener");
3900            }
3901        }
3902        if (remainingListeners) {
3903            switchToForeground();
3904        }
3905    }
3906
3907    public void removeOnAccountListChangedListener(OnAccountUpdate listener) {
3908        final boolean remainingListeners;
3909        synchronized (LISTENER_LOCK) {
3910            this.mOnAccountUpdates.remove(listener);
3911            remainingListeners = checkListeners();
3912        }
3913        if (remainingListeners) {
3914            switchToBackground();
3915        }
3916    }
3917
3918    public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
3919        final boolean remainingListeners;
3920        synchronized (LISTENER_LOCK) {
3921            remainingListeners = checkListeners();
3922            if (!this.mOnCaptchaRequested.add(listener)) {
3923                Log.w(
3924                        Config.LOGTAG,
3925                        listener.getClass().getName()
3926                                + " is already registered as OnCaptchaRequestListener");
3927            }
3928        }
3929        if (remainingListeners) {
3930            switchToForeground();
3931        }
3932    }
3933
3934    public void removeOnCaptchaRequestedListener(OnCaptchaRequested listener) {
3935        final boolean remainingListeners;
3936        synchronized (LISTENER_LOCK) {
3937            this.mOnCaptchaRequested.remove(listener);
3938            remainingListeners = checkListeners();
3939        }
3940        if (remainingListeners) {
3941            switchToBackground();
3942        }
3943    }
3944
3945    public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
3946        final boolean remainingListeners;
3947        synchronized (LISTENER_LOCK) {
3948            remainingListeners = checkListeners();
3949            if (!this.mOnRosterUpdates.add(listener)) {
3950                Log.w(
3951                        Config.LOGTAG,
3952                        listener.getClass().getName()
3953                                + " is already registered as OnRosterUpdateListener");
3954            }
3955        }
3956        if (remainingListeners) {
3957            switchToForeground();
3958        }
3959    }
3960
3961    public void removeOnRosterUpdateListener(final OnRosterUpdate listener) {
3962        final boolean remainingListeners;
3963        synchronized (LISTENER_LOCK) {
3964            this.mOnRosterUpdates.remove(listener);
3965            remainingListeners = checkListeners();
3966        }
3967        if (remainingListeners) {
3968            switchToBackground();
3969        }
3970    }
3971
3972    public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
3973        final boolean remainingListeners;
3974        synchronized (LISTENER_LOCK) {
3975            remainingListeners = checkListeners();
3976            if (!this.mOnUpdateBlocklist.add(listener)) {
3977                Log.w(
3978                        Config.LOGTAG,
3979                        listener.getClass().getName()
3980                                + " is already registered as OnUpdateBlocklistListener");
3981            }
3982        }
3983        if (remainingListeners) {
3984            switchToForeground();
3985        }
3986    }
3987
3988    public void removeOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
3989        final boolean remainingListeners;
3990        synchronized (LISTENER_LOCK) {
3991            this.mOnUpdateBlocklist.remove(listener);
3992            remainingListeners = checkListeners();
3993        }
3994        if (remainingListeners) {
3995            switchToBackground();
3996        }
3997    }
3998
3999    public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
4000        final boolean remainingListeners;
4001        synchronized (LISTENER_LOCK) {
4002            remainingListeners = checkListeners();
4003            if (!this.mOnKeyStatusUpdated.add(listener)) {
4004                Log.w(
4005                        Config.LOGTAG,
4006                        listener.getClass().getName()
4007                                + " is already registered as OnKeyStatusUpdateListener");
4008            }
4009        }
4010        if (remainingListeners) {
4011            switchToForeground();
4012        }
4013    }
4014
4015    public void removeOnNewKeysAvailableListener(final OnKeyStatusUpdated listener) {
4016        final boolean remainingListeners;
4017        synchronized (LISTENER_LOCK) {
4018            this.mOnKeyStatusUpdated.remove(listener);
4019            remainingListeners = checkListeners();
4020        }
4021        if (remainingListeners) {
4022            switchToBackground();
4023        }
4024    }
4025
4026    public void setOnRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
4027        final boolean remainingListeners;
4028        synchronized (LISTENER_LOCK) {
4029            remainingListeners = checkListeners();
4030            if (!this.onJingleRtpConnectionUpdate.add(listener)) {
4031                Log.w(
4032                        Config.LOGTAG,
4033                        listener.getClass().getName()
4034                                + " is already registered as OnJingleRtpConnectionUpdate");
4035            }
4036        }
4037        if (remainingListeners) {
4038            switchToForeground();
4039        }
4040    }
4041
4042    public void removeRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
4043        final boolean remainingListeners;
4044        synchronized (LISTENER_LOCK) {
4045            this.onJingleRtpConnectionUpdate.remove(listener);
4046            remainingListeners = checkListeners();
4047        }
4048        if (remainingListeners) {
4049            switchToBackground();
4050        }
4051    }
4052
4053    public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
4054        final boolean remainingListeners;
4055        synchronized (LISTENER_LOCK) {
4056            remainingListeners = checkListeners();
4057            if (!this.mOnMucRosterUpdate.add(listener)) {
4058                Log.w(
4059                        Config.LOGTAG,
4060                        listener.getClass().getName()
4061                                + " is already registered as OnMucRosterListener");
4062            }
4063        }
4064        if (remainingListeners) {
4065            switchToForeground();
4066        }
4067    }
4068
4069    public void removeOnMucRosterUpdateListener(final OnMucRosterUpdate listener) {
4070        final boolean remainingListeners;
4071        synchronized (LISTENER_LOCK) {
4072            this.mOnMucRosterUpdate.remove(listener);
4073            remainingListeners = checkListeners();
4074        }
4075        if (remainingListeners) {
4076            switchToBackground();
4077        }
4078    }
4079
4080    public boolean checkListeners() {
4081        return (this.mOnAccountUpdates.isEmpty()
4082                && this.mOnConversationUpdates.isEmpty()
4083                && this.mOnRosterUpdates.isEmpty()
4084                && this.mOnCaptchaRequested.isEmpty()
4085                && this.mOnMucRosterUpdate.isEmpty()
4086                && this.mOnUpdateBlocklist.isEmpty()
4087                && this.mOnShowErrorToasts.isEmpty()
4088                && this.onJingleRtpConnectionUpdate.isEmpty()
4089                && this.mOnKeyStatusUpdated.isEmpty());
4090    }
4091
4092    private void switchToForeground() {
4093        toggleSoftDisabled(false);
4094        final boolean broadcastLastActivity = broadcastLastActivity();
4095        for (Conversation conversation : getConversations()) {
4096            if (conversation.getMode() == Conversation.MODE_MULTI) {
4097                conversation.getMucOptions().resetChatState();
4098            } else {
4099                conversation.setIncomingChatState(Config.DEFAULT_CHAT_STATE);
4100            }
4101        }
4102        for (Account account : getAccounts()) {
4103            if (account.getStatus() == Account.State.ONLINE) {
4104                account.deactivateGracePeriod();
4105                final XmppConnection connection = account.getXmppConnection();
4106                if (connection != null) {
4107                    if (connection.getFeatures().csi()) {
4108                        connection.sendActive();
4109                    }
4110                    if (broadcastLastActivity) {
4111                        sendPresence(
4112                                account,
4113                                false); // send new presence but don't include idle because we are
4114                        // not
4115                    }
4116                }
4117            }
4118        }
4119        Log.d(Config.LOGTAG, "app switched into foreground");
4120    }
4121
4122    private void switchToBackground() {
4123        final boolean broadcastLastActivity = broadcastLastActivity();
4124        if (broadcastLastActivity) {
4125            mLastActivity = System.currentTimeMillis();
4126            final SharedPreferences.Editor editor = getPreferences().edit();
4127            editor.putLong(SETTING_LAST_ACTIVITY_TS, mLastActivity);
4128            editor.apply();
4129        }
4130        for (Account account : getAccounts()) {
4131            if (account.getStatus() == Account.State.ONLINE) {
4132                XmppConnection connection = account.getXmppConnection();
4133                if (connection != null) {
4134                    if (broadcastLastActivity) {
4135                        sendPresence(account, true);
4136                    }
4137                    if (connection.getFeatures().csi()) {
4138                        connection.sendInactive();
4139                    }
4140                }
4141            }
4142        }
4143        this.mNotificationService.setIsInForeground(false);
4144        Log.d(Config.LOGTAG, "app switched into background");
4145    }
4146
4147    public void connectMultiModeConversations(Account account) {
4148        List<Conversation> conversations = getConversations();
4149        for (Conversation conversation : conversations) {
4150            if (conversation.getMode() == Conversation.MODE_MULTI
4151                    && conversation.getAccount() == account) {
4152                joinMuc(conversation);
4153            }
4154        }
4155    }
4156
4157    public void mucSelfPingAndRejoin(final Conversation conversation) {
4158        final Account account = conversation.getAccount();
4159        synchronized (account.inProgressConferenceJoins) {
4160            if (account.inProgressConferenceJoins.contains(conversation)) {
4161                Log.d(
4162                        Config.LOGTAG,
4163                        account.getJid().asBareJid()
4164                                + ": canceling muc self ping because join is already under way");
4165                return;
4166            }
4167        }
4168        synchronized (account.inProgressConferencePings) {
4169            if (!account.inProgressConferencePings.add(conversation)) {
4170                Log.d(
4171                        Config.LOGTAG,
4172                        account.getJid().asBareJid()
4173                                + ": canceling muc self ping because ping is already under way");
4174                return;
4175            }
4176        }
4177        final Jid self = conversation.getMucOptions().getSelf().getFullJid();
4178        final Iq ping = new Iq(Iq.Type.GET);
4179        ping.setTo(self);
4180        ping.addChild("ping", Namespace.PING);
4181        sendIqPacket(
4182                conversation.getAccount(),
4183                ping,
4184                (response) -> {
4185                    if (response.getType() == Iq.Type.ERROR) {
4186                        final var error = response.getError();
4187                        if (error == null
4188                                || error.hasChild("service-unavailable")
4189                                || error.hasChild("feature-not-implemented")
4190                                || error.hasChild("item-not-found")) {
4191                            Log.d(
4192                                    Config.LOGTAG,
4193                                    account.getJid().asBareJid()
4194                                            + ": ping to "
4195                                            + self
4196                                            + " came back as ignorable error");
4197                        } else {
4198                            Log.d(
4199                                    Config.LOGTAG,
4200                                    account.getJid().asBareJid()
4201                                            + ": ping to "
4202                                            + self
4203                                            + " failed. attempting rejoin");
4204                            joinMuc(conversation);
4205                        }
4206                    } else if (response.getType() == Iq.Type.RESULT) {
4207                        Log.d(
4208                                Config.LOGTAG,
4209                                account.getJid().asBareJid()
4210                                        + ": ping to "
4211                                        + self
4212                                        + " came back fine");
4213                    }
4214                    synchronized (account.inProgressConferencePings) {
4215                        account.inProgressConferencePings.remove(conversation);
4216                    }
4217                });
4218    }
4219
4220    public void joinMuc(Conversation conversation) {
4221        joinMuc(conversation, null, false);
4222    }
4223
4224    public void joinMuc(Conversation conversation, boolean followedInvite) {
4225        joinMuc(conversation, null, followedInvite);
4226    }
4227
4228    private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
4229        joinMuc(conversation, onConferenceJoined, false);
4230    }
4231
4232    private void joinMuc(
4233            final Conversation conversation,
4234            final OnConferenceJoined onConferenceJoined,
4235            final boolean followedInvite) {
4236        final Account account = conversation.getAccount();
4237        synchronized (account.pendingConferenceJoins) {
4238            account.pendingConferenceJoins.remove(conversation);
4239        }
4240        synchronized (account.pendingConferenceLeaves) {
4241            account.pendingConferenceLeaves.remove(conversation);
4242        }
4243        if (account.getStatus() == Account.State.ONLINE) {
4244            synchronized (account.inProgressConferenceJoins) {
4245                account.inProgressConferenceJoins.add(conversation);
4246            }
4247            if (Config.MUC_LEAVE_BEFORE_JOIN) {
4248                sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions()));
4249            }
4250            conversation.resetMucOptions();
4251            if (onConferenceJoined != null) {
4252                conversation.getMucOptions().flagNoAutoPushConfiguration();
4253            }
4254            conversation.setHasMessagesLeftOnServer(false);
4255            fetchConferenceConfiguration(
4256                    conversation,
4257                    new OnConferenceConfigurationFetched() {
4258
4259                        private void join(Conversation conversation) {
4260                            Account account = conversation.getAccount();
4261                            final MucOptions mucOptions = conversation.getMucOptions();
4262
4263                            if (mucOptions.nonanonymous()
4264                                    && !mucOptions.membersOnly()
4265                                    && !conversation.getBooleanAttribute(
4266                                            "accept_non_anonymous", false)) {
4267                                synchronized (account.inProgressConferenceJoins) {
4268                                    account.inProgressConferenceJoins.remove(conversation);
4269                                }
4270                                mucOptions.setError(MucOptions.Error.NON_ANONYMOUS);
4271                                updateConversationUi();
4272                                if (onConferenceJoined != null) {
4273                                    onConferenceJoined.onConferenceJoined(conversation);
4274                                }
4275                                return;
4276                            }
4277
4278                            final Jid joinJid = mucOptions.getSelf().getFullJid();
4279                            Log.d(
4280                                    Config.LOGTAG,
4281                                    account.getJid().asBareJid().toString()
4282                                            + ": joining conversation "
4283                                            + joinJid.toString());
4284                            final var packet =
4285                                    mPresenceGenerator.selfPresence(
4286                                            account,
4287                                            Presence.Status.ONLINE,
4288                                            mucOptions.nonanonymous()
4289                                                    || onConferenceJoined != null,
4290                                            mucOptions.getSelf().getNick());
4291                            packet.setTo(joinJid);
4292                            Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
4293                            if (conversation.getMucOptions().getPassword() != null) {
4294                                x.addChild("password").setContent(mucOptions.getPassword());
4295                            }
4296
4297                            if (mucOptions.mamSupport()) {
4298                                // Use MAM instead of the limited muc history to get history
4299                                x.addChild("history").setAttribute("maxchars", "0");
4300                            } else {
4301                                // Fallback to muc history
4302                                x.addChild("history")
4303                                        .setAttribute(
4304                                                "since",
4305                                                PresenceGenerator.getTimestamp(
4306                                                        conversation
4307                                                                .getLastMessageTransmitted()
4308                                                                .getTimestamp()));
4309                            }
4310                            sendPresencePacket(account, packet);
4311                            if (onConferenceJoined != null) {
4312                                onConferenceJoined.onConferenceJoined(conversation);
4313                            }
4314                            if (!joinJid.equals(conversation.getJid())) {
4315                                conversation.setContactJid(joinJid);
4316                                databaseBackend.updateConversation(conversation);
4317                            }
4318
4319                            maybeRegisterWithMuc(conversation, null);
4320
4321                            if (mucOptions.mamSupport()) {
4322                                getMessageArchiveService().catchupMUC(conversation);
4323                            }
4324                            fetchConferenceMembers(conversation);
4325                            if (mucOptions.isPrivateAndNonAnonymous()) {
4326                                if (followedInvite) {
4327                                    final Bookmark bookmark = conversation.getBookmark();
4328                                    if (bookmark != null) {
4329                                        if (!bookmark.autojoin()) {
4330                                            bookmark.setAutojoin(true);
4331                                            createBookmark(account, bookmark);
4332                                        }
4333                                    } else {
4334                                        saveConversationAsBookmark(conversation, null);
4335                                    }
4336                                }
4337                            }
4338                            synchronized (account.inProgressConferenceJoins) {
4339                                account.inProgressConferenceJoins.remove(conversation);
4340                                sendUnsentMessages(conversation);
4341                            }
4342                        }
4343
4344                        @Override
4345                        public void onConferenceConfigurationFetched(Conversation conversation) {
4346                            if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
4347                                Log.d(
4348                                        Config.LOGTAG,
4349                                        account.getJid().asBareJid()
4350                                                + ": conversation ("
4351                                                + conversation.getJid()
4352                                                + ") got archived before IQ result");
4353                                return;
4354                            }
4355                            join(conversation);
4356                        }
4357
4358                        @Override
4359                        public void onFetchFailed(
4360                                final Conversation conversation, final String errorCondition) {
4361                            if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
4362                                Log.d(
4363                                        Config.LOGTAG,
4364                                        account.getJid().asBareJid()
4365                                                + ": conversation ("
4366                                                + conversation.getJid()
4367                                                + ") got archived before IQ result");
4368                                return;
4369                            }
4370                            if ("remote-server-not-found".equals(errorCondition)) {
4371                                synchronized (account.inProgressConferenceJoins) {
4372                                    account.inProgressConferenceJoins.remove(conversation);
4373                                }
4374                                conversation
4375                                        .getMucOptions()
4376                                        .setError(MucOptions.Error.SERVER_NOT_FOUND);
4377                                updateConversationUi();
4378                            } else {
4379                                join(conversation);
4380                                fetchConferenceConfiguration(conversation);
4381                            }
4382                        }
4383                    });
4384            updateConversationUi();
4385        } else {
4386            synchronized (account.pendingConferenceJoins) {
4387                account.pendingConferenceJoins.add(conversation);
4388            }
4389            conversation.resetMucOptions();
4390            conversation.setHasMessagesLeftOnServer(false);
4391            updateConversationUi();
4392        }
4393    }
4394
4395    private void fetchConferenceMembers(final Conversation conversation) {
4396        final Account account = conversation.getAccount();
4397        final AxolotlService axolotlService = account.getAxolotlService();
4398        final var affiliations = new ArrayList<String>();
4399        affiliations.add("outcast");
4400        if (conversation.getMucOptions().isPrivateAndNonAnonymous()) affiliations.addAll(List.of("member", "admin", "owner"));
4401        final Consumer<Iq> callback =
4402                new Consumer<Iq>() {
4403
4404                    private int i = 0;
4405                    private boolean success = true;
4406
4407                    @Override
4408                    public void accept(Iq response) {
4409                        final boolean omemoEnabled =
4410                                conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL;
4411                        Element query = response.query("http://jabber.org/protocol/muc#admin");
4412                        if (response.getType() == Iq.Type.RESULT && query != null) {
4413                            for (Element child : query.getChildren()) {
4414                                if ("item".equals(child.getName())) {
4415                                    MucOptions.User user =
4416                                            AbstractParser.parseItem(conversation, child);
4417                                    user.setOnline(false);
4418                                    if (!user.realJidMatchesAccount()) {
4419                                        boolean isNew =
4420                                                conversation.getMucOptions().updateUser(user);
4421                                        Contact contact = user.getContact();
4422                                        if (omemoEnabled
4423                                                && isNew
4424                                                && user.getRealJid() != null
4425                                                && (contact == null
4426                                                        || !contact.mutualPresenceSubscription())
4427                                                && axolotlService.hasEmptyDeviceList(
4428                                                        user.getRealJid())) {
4429                                            axolotlService.fetchDeviceIds(user.getRealJid());
4430                                        }
4431                                    }
4432                                }
4433                            }
4434                        } else {
4435                            success = false;
4436                            Log.d(
4437                                    Config.LOGTAG,
4438                                    account.getJid().asBareJid()
4439                                            + ": could not request affiliation "
4440                                            + affiliations.get(i)
4441                                            + " in "
4442                                            + conversation.getJid().asBareJid());
4443                        }
4444                        ++i;
4445                        if (i >= affiliations.size()) {
4446                            final var mucOptions = conversation.getMucOptions();
4447                            List<Jid> members = mucOptions.getMembers(true);
4448                            if (success) {
4449                                List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
4450                                boolean changed = false;
4451                                for (ListIterator<Jid> iterator = cryptoTargets.listIterator();
4452                                        iterator.hasNext(); ) {
4453                                    Jid jid = iterator.next();
4454                                    if (!members.contains(jid)
4455                                            && !members.contains(jid.getDomain())) {
4456                                        iterator.remove();
4457                                        Log.d(
4458                                                Config.LOGTAG,
4459                                                account.getJid().asBareJid()
4460                                                        + ": removed "
4461                                                        + jid
4462                                                        + " from crypto targets of "
4463                                                        + conversation.getName());
4464                                        changed = true;
4465                                    }
4466                                }
4467                                if (changed) {
4468                                    conversation.setAcceptedCryptoTargets(cryptoTargets);
4469                                    updateConversation(conversation);
4470                                }
4471                            }
4472                            getAvatarService().clear(mucOptions);
4473                            updateMucRosterUi();
4474                            updateConversationUi();
4475                        }
4476                    }
4477                };
4478        for (String affiliation : affiliations) {
4479            sendIqPacket(
4480                    account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
4481        }
4482        Log.d(
4483                Config.LOGTAG,
4484                account.getJid().asBareJid() + ": fetching members for " + conversation.getName());
4485    }
4486
4487    public void providePasswordForMuc(final Conversation conversation, final String password) {
4488        if (conversation.getMode() == Conversation.MODE_MULTI) {
4489            conversation.getMucOptions().setPassword(password);
4490            if (conversation.getBookmark() != null) {
4491                final Bookmark bookmark = conversation.getBookmark();
4492                bookmark.setAutojoin(true);
4493                createBookmark(conversation.getAccount(), bookmark);
4494            }
4495            updateConversation(conversation);
4496            joinMuc(conversation);
4497        }
4498    }
4499
4500    public void deleteAvatar(final Account account) {
4501        final AtomicBoolean executed = new AtomicBoolean(false);
4502        final Runnable onDeleted =
4503                () -> {
4504                    if (executed.compareAndSet(false, true)) {
4505                        account.setAvatar(null);
4506                        databaseBackend.updateAccount(account);
4507                        getAvatarService().clear(account);
4508                        updateAccountUi();
4509                    }
4510                };
4511        deleteVcardAvatar(account, onDeleted);
4512        deletePepNode(account, Namespace.AVATAR_DATA);
4513        deletePepNode(account, Namespace.AVATAR_METADATA, onDeleted);
4514    }
4515
4516    public void deletePepNode(final Account account, final String node) {
4517        deletePepNode(account, node, null);
4518    }
4519
4520    private void deletePepNode(final Account account, final String node, final Runnable runnable) {
4521        final Iq request = mIqGenerator.deleteNode(node);
4522        sendIqPacket(
4523                account,
4524                request,
4525                (packet) -> {
4526                    if (packet.getType() == Iq.Type.RESULT) {
4527                        Log.d(
4528                                Config.LOGTAG,
4529                                account.getJid().asBareJid()
4530                                        + ": successfully deleted pep node "
4531                                        + node);
4532                        if (runnable != null) {
4533                            runnable.run();
4534                        }
4535                    } else {
4536                        Log.d(
4537                                Config.LOGTAG,
4538                                account.getJid().asBareJid() + ": failed to delete " + packet);
4539                    }
4540                });
4541    }
4542
4543    private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) {
4544        final Iq retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid());
4545        sendIqPacket(
4546                account,
4547                retrieveVcard,
4548                (response) -> {
4549                    if (response.getType() != Iq.Type.RESULT) {
4550                        Log.d(
4551                                Config.LOGTAG,
4552                                account.getJid().asBareJid() + ": no vCard set. nothing to do");
4553                        return;
4554                    }
4555                    final Element vcard = response.findChild("vCard", "vcard-temp");
4556                    if (vcard == null) {
4557                        Log.d(
4558                                Config.LOGTAG,
4559                                account.getJid().asBareJid() + ": no vCard set. nothing to do");
4560                        return;
4561                    }
4562                    Element photo = vcard.findChild("PHOTO");
4563                    if (photo == null) {
4564                        photo = vcard.addChild("PHOTO");
4565                    }
4566                    photo.clearChildren();
4567                    final Iq publication = new Iq(Iq.Type.SET);
4568                    publication.setTo(account.getJid().asBareJid());
4569                    publication.addChild(vcard);
4570                    sendIqPacket(
4571                            account,
4572                            publication,
4573                            (publicationResponse) -> {
4574                                if (publicationResponse.getType() == Iq.Type.RESULT) {
4575                                    Log.d(
4576                                            Config.LOGTAG,
4577                                            account.getJid().asBareJid()
4578                                                    + ": successfully deleted vcard avatar");
4579                                    runnable.run();
4580                                } else {
4581                                    Log.d(
4582                                            Config.LOGTAG,
4583                                            "failed to publish vcard "
4584                                                    + publicationResponse.getErrorCondition());
4585                                }
4586                            });
4587                });
4588    }
4589
4590    private boolean hasEnabledAccounts() {
4591        if (this.accounts == null) {
4592            return false;
4593        }
4594        for (final Account account : this.accounts) {
4595            if (account.isConnectionEnabled()) {
4596                return true;
4597            }
4598        }
4599        return false;
4600    }
4601
4602    public void getAttachments(
4603            final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) {
4604        getAttachments(
4605                conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded);
4606    }
4607
4608    public void getAttachments(
4609            final Account account,
4610            final Jid jid,
4611            final int limit,
4612            final OnMediaLoaded onMediaLoaded) {
4613        getAttachments(account.getUuid(), jid.asBareJid(), limit, onMediaLoaded);
4614    }
4615
4616    public void getAttachments(
4617            final String account,
4618            final Jid jid,
4619            final int limit,
4620            final OnMediaLoaded onMediaLoaded) {
4621        new Thread(
4622                        () ->
4623                                onMediaLoaded.onMediaLoaded(
4624                                        fileBackend.convertToAttachments(
4625                                                databaseBackend.getRelativeFilePaths(
4626                                                        account, jid, limit))))
4627                .start();
4628    }
4629
4630    public void persistSelfNick(final MucOptions.User self, final boolean modified) {
4631        final Conversation conversation = self.getConversation();
4632        final Account account = conversation.getAccount();
4633        final Jid full = self.getFullJid();
4634        if (!full.equals(conversation.getJid())) {
4635            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persisting full jid " + full);
4636            conversation.setContactJid(full);
4637            databaseBackend.updateConversation(conversation);
4638        }
4639
4640        final String nick = self.getNick();
4641        final Bookmark bookmark = conversation.getBookmark();
4642        if (bookmark == null || !modified) {
4643            return;
4644        }
4645        final String defaultNick = MucOptions.defaultNick(account);
4646        if (nick.equals(defaultNick) || nick.equals(bookmark.getNick())) {
4647            return;
4648        }
4649        Log.d(
4650                Config.LOGTAG,
4651                account.getJid().asBareJid()
4652                        + ": persist nick '"
4653                        + full.getResource()
4654                        + "' into bookmark for "
4655                        + conversation.getJid().asBareJid());
4656        bookmark.setNick(nick);
4657        createBookmark(bookmark.getAccount(), bookmark);
4658    }
4659
4660    public void presenceToMuc(final Conversation conversation) {
4661        final MucOptions options = conversation.getMucOptions();
4662        if (options.online()) {
4663            Account account = conversation.getAccount();
4664            final Jid joinJid = options.getSelf().getFullJid();
4665            final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), options.getSelf().getNick());
4666            packet.setTo(joinJid);
4667            sendPresencePacket(account, packet);
4668        }
4669    }
4670
4671    public boolean renameInMuc(
4672            final Conversation conversation,
4673            final String nick,
4674            final UiCallback<Conversation> callback) {
4675        final Account account = conversation.getAccount();
4676        final Bookmark bookmark = conversation.getBookmark();
4677        final MucOptions options = conversation.getMucOptions();
4678        final Jid joinJid = options.createJoinJid(nick);
4679        if (joinJid == null) {
4680            return false;
4681        }
4682        if (options.online()) {
4683            maybeRegisterWithMuc(conversation, nick);
4684            options.setOnRenameListener(
4685                    new OnRenameListener() {
4686
4687                        @Override
4688                        public void onSuccess() {
4689                            final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick);
4690                            packet.setTo(joinJid);
4691                            sendPresencePacket(account, packet);
4692                            callback.success(conversation);
4693                        }
4694
4695                        @Override
4696                        public void onFailure() {
4697                            callback.error(R.string.nick_in_use, conversation);
4698                        }
4699                    });
4700
4701            final var packet =
4702                    mPresenceGenerator.selfPresence(
4703                            account, Presence.Status.ONLINE, options.nonanonymous(), nick);
4704            packet.setTo(joinJid);
4705            sendPresencePacket(account, packet);
4706            if (nick.equals(MucOptions.defaultNick(account))
4707                    && bookmark != null
4708                    && bookmark.getNick() != null) {
4709                Log.d(
4710                        Config.LOGTAG,
4711                        account.getJid().asBareJid()
4712                                + ": removing nick from bookmark for "
4713                                + bookmark.getJid());
4714                bookmark.setNick(null);
4715                createBookmark(account, bookmark);
4716            }
4717        } else {
4718            conversation.setContactJid(joinJid);
4719            databaseBackend.updateConversation(conversation);
4720            if (account.getStatus() == Account.State.ONLINE) {
4721                if (bookmark != null) {
4722                    bookmark.setNick(nick);
4723                    createBookmark(account, bookmark);
4724                }
4725                joinMuc(conversation);
4726            }
4727        }
4728        return true;
4729    }
4730
4731    public void checkMucRequiresRename() {
4732        synchronized (this.conversations) {
4733            for (final Conversation conversation : this.conversations) {
4734                if (conversation.getMode() == Conversational.MODE_MULTI) {
4735                    checkMucRequiresRename(conversation);
4736                }
4737            }
4738        }
4739    }
4740
4741    private void checkMucRequiresRename(final Conversation conversation) {
4742        final var options = conversation.getMucOptions();
4743        if (!options.online()) {
4744            return;
4745        }
4746        final var account = conversation.getAccount();
4747        final String current = options.getActualNick();
4748        final String proposed = options.getProposedNickPure();
4749        if (current == null || current.equals(proposed)) {
4750            return;
4751        }
4752        final Jid joinJid = options.createJoinJid(proposed);
4753        Log.d(
4754                Config.LOGTAG,
4755                String.format(
4756                        "%s: muc rename required %s (was: %s)",
4757                        account.getJid().asBareJid(), joinJid, current));
4758        final var packet =
4759                mPresenceGenerator.selfPresence(
4760                        account, Presence.Status.ONLINE, options.nonanonymous(), proposed);
4761        packet.setTo(joinJid);
4762        sendPresencePacket(account, packet);
4763    }
4764
4765    public void leaveMuc(Conversation conversation) {
4766        leaveMuc(conversation, false);
4767    }
4768
4769    private void leaveMuc(Conversation conversation, boolean now) {
4770        final Account account = conversation.getAccount();
4771        synchronized (account.pendingConferenceJoins) {
4772            account.pendingConferenceJoins.remove(conversation);
4773        }
4774        synchronized (account.pendingConferenceLeaves) {
4775            account.pendingConferenceLeaves.remove(conversation);
4776        }
4777        if (account.getStatus() == Account.State.ONLINE || now) {
4778            sendPresencePacket(
4779                    conversation.getAccount(),
4780                    mPresenceGenerator.leave(conversation.getMucOptions()));
4781            conversation.getMucOptions().setOffline();
4782            Bookmark bookmark = conversation.getBookmark();
4783            if (bookmark != null) {
4784                bookmark.setConversation(null);
4785            }
4786            Log.d(
4787                    Config.LOGTAG,
4788                    conversation.getAccount().getJid().asBareJid()
4789                            + ": leaving muc "
4790                            + conversation.getJid());
4791        } else {
4792            synchronized (account.pendingConferenceLeaves) {
4793                account.pendingConferenceLeaves.add(conversation);
4794            }
4795        }
4796    }
4797
4798    public String findConferenceServer(final Account account) {
4799        String server;
4800        if (account.getXmppConnection() != null) {
4801            server = account.getXmppConnection().getMucServer();
4802            if (server != null) {
4803                return server;
4804            }
4805        }
4806        for (Account other : getAccounts()) {
4807            if (other != account && other.getXmppConnection() != null) {
4808                server = other.getXmppConnection().getMucServer();
4809                if (server != null) {
4810                    return server;
4811                }
4812            }
4813        }
4814        return null;
4815    }
4816
4817    public void createPublicChannel(
4818            final Account account,
4819            final String name,
4820            final Jid address,
4821            final UiCallback<Conversation> callback) {
4822        joinMuc(
4823                findOrCreateConversation(account, address, true, false, true),
4824                conversation -> {
4825                    final Bundle configuration = IqGenerator.defaultChannelConfiguration();
4826                    if (!TextUtils.isEmpty(name)) {
4827                        configuration.putString("muc#roomconfig_roomname", name);
4828                    }
4829                    pushConferenceConfiguration(
4830                            conversation,
4831                            configuration,
4832                            new OnConfigurationPushed() {
4833                                @Override
4834                                public void onPushSucceeded() {
4835                                    saveConversationAsBookmark(conversation, name);
4836                                    callback.success(conversation);
4837                                }
4838
4839                                @Override
4840                                public void onPushFailed() {
4841                                    if (conversation
4842                                            .getMucOptions()
4843                                            .getSelf()
4844                                            .getAffiliation()
4845                                            .ranks(MucOptions.Affiliation.OWNER)) {
4846                                        callback.error(
4847                                                R.string.unable_to_set_channel_configuration,
4848                                                conversation);
4849                                    } else {
4850                                        callback.error(
4851                                                R.string.joined_an_existing_channel, conversation);
4852                                    }
4853                                }
4854                            });
4855                });
4856    }
4857
4858    public boolean createAdhocConference(
4859            final Account account,
4860            final String name,
4861            final Iterable<Jid> jids,
4862            final UiCallback<Conversation> callback) {
4863        Log.d(
4864                Config.LOGTAG,
4865                account.getJid().asBareJid().toString()
4866                        + ": creating adhoc conference with "
4867                        + jids.toString());
4868        if (account.getStatus() == Account.State.ONLINE) {
4869            try {
4870                String server = findConferenceServer(account);
4871                if (server == null) {
4872                    if (callback != null) {
4873                        callback.error(R.string.no_conference_server_found, null);
4874                    }
4875                    return false;
4876                }
4877                final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null);
4878                final Conversation conversation =
4879                        findOrCreateConversation(account, jid, true, false, true);
4880                joinMuc(
4881                        conversation,
4882                        new OnConferenceJoined() {
4883                            @Override
4884                            public void onConferenceJoined(final Conversation conversation) {
4885                                final Bundle configuration =
4886                                        IqGenerator.defaultGroupChatConfiguration();
4887                                if (!TextUtils.isEmpty(name)) {
4888                                    configuration.putString("muc#roomconfig_roomname", name);
4889                                }
4890                                pushConferenceConfiguration(
4891                                        conversation,
4892                                        configuration,
4893                                        new OnConfigurationPushed() {
4894                                            @Override
4895                                            public void onPushSucceeded() {
4896                                                for (Jid invite : jids) {
4897                                                    invite(conversation, invite);
4898                                                }
4899                                                for (String resource :
4900                                                        account.getSelfContact()
4901                                                                .getPresences()
4902                                                                .toResourceArray()) {
4903                                                    Jid other =
4904                                                            account.getJid().withResource(resource);
4905                                                    Log.d(
4906                                                            Config.LOGTAG,
4907                                                            account.getJid().asBareJid()
4908                                                                    + ": sending direct invite to "
4909                                                                    + other);
4910                                                    directInvite(conversation, other);
4911                                                }
4912                                                saveConversationAsBookmark(conversation, name);
4913                                                if (callback != null) {
4914                                                    callback.success(conversation);
4915                                                }
4916                                            }
4917
4918                                            @Override
4919                                            public void onPushFailed() {
4920                                                archiveConversation(conversation);
4921                                                if (callback != null) {
4922                                                    callback.error(
4923                                                            R.string.conference_creation_failed,
4924                                                            conversation);
4925                                                }
4926                                            }
4927                                        });
4928                            }
4929                        });
4930                return true;
4931            } catch (IllegalArgumentException e) {
4932                if (callback != null) {
4933                    callback.error(R.string.conference_creation_failed, null);
4934                }
4935                return false;
4936            }
4937        } else {
4938            if (callback != null) {
4939                callback.error(R.string.not_connected_try_again, null);
4940            }
4941            return false;
4942        }
4943    }
4944
4945    public void checkIfMuc(final Account account, final Jid jid, Consumer<Boolean> cb) {
4946        if (jid.isDomainJid()) {
4947            // Spec basically says MUC needs to have a node
4948            // And also specifies that MUC and MUC service should have the same identity...
4949            cb.accept(false);
4950            return;
4951        }
4952
4953        final var request = mIqGenerator.queryDiscoInfo(jid.asBareJid());
4954        sendIqPacket(account, request, (reply) -> {
4955            final var result = new ServiceDiscoveryResult(reply);
4956            cb.accept(
4957                result.getFeatures().contains("http://jabber.org/protocol/muc") &&
4958                result.hasIdentity("conference", null)
4959            );
4960        });
4961    }
4962
4963    public void fetchConferenceConfiguration(final Conversation conversation) {
4964        fetchConferenceConfiguration(conversation, null);
4965    }
4966
4967    public void fetchConferenceConfiguration(
4968            final Conversation conversation, final OnConferenceConfigurationFetched callback) {
4969        final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid());
4970        final var account = conversation.getAccount();
4971        sendIqPacket(
4972                account,
4973                request,
4974                response -> {
4975                    if (response.getType() == Iq.Type.RESULT) {
4976                        final MucOptions mucOptions = conversation.getMucOptions();
4977                        final Bookmark bookmark = conversation.getBookmark();
4978                        final boolean sameBefore =
4979                                StringUtils.equals(
4980                                        bookmark == null ? null : bookmark.getBookmarkName(),
4981                                        mucOptions.getName());
4982
4983                        final var hadOccupantId = mucOptions.occupantId();
4984                        if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(response))) {
4985                            Log.d(
4986                                    Config.LOGTAG,
4987                                    account.getJid().asBareJid()
4988                                            + ": muc configuration changed for "
4989                                            + conversation.getJid().asBareJid());
4990                            updateConversation(conversation);
4991                        }
4992
4993                        final var hasOccupantId = mucOptions.occupantId();
4994
4995                        if (!hadOccupantId && hasOccupantId && mucOptions.online()) {
4996                            final var me = mucOptions.getSelf().getFullJid();
4997                            Log.d(
4998                                    Config.LOGTAG,
4999                                    account.getJid().asBareJid()
5000                                            + ": gained support for occupant-id in "
5001                                            + me
5002                                            + ". resending presence");
5003                            final var packet =
5004                                    mPresenceGenerator.selfPresence(
5005                                            account,
5006                                            Presence.Status.ONLINE,
5007                                            mucOptions.nonanonymous(), mucOptions.getSelf().getNick());
5008                            packet.setTo(me);
5009                            sendPresencePacket(account, packet);
5010                        }
5011
5012                        if (bookmark != null
5013                                && (sameBefore || bookmark.getBookmarkName() == null)) {
5014                            if (bookmark.setBookmarkName(
5015                                    StringUtils.nullOnEmpty(mucOptions.getName()))) {
5016                                createBookmark(account, bookmark);
5017                            }
5018                        }
5019
5020                        if (callback != null) {
5021                            callback.onConferenceConfigurationFetched(conversation);
5022                        }
5023
5024                        updateConversationUi();
5025                    } else if (response.getType() == Iq.Type.TIMEOUT) {
5026                        Log.d(
5027                                Config.LOGTAG,
5028                                account.getJid().asBareJid()
5029                                        + ": received timeout waiting for conference configuration"
5030                                        + " fetch");
5031                    } else {
5032                        if (callback != null) {
5033                            callback.onFetchFailed(conversation, response.getErrorCondition());
5034                        }
5035                    }
5036                });
5037    }
5038
5039    public void pushNodeConfiguration(
5040            Account account,
5041            final String node,
5042            final Bundle options,
5043            final OnConfigurationPushed callback) {
5044        pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback);
5045    }
5046
5047    public void pushNodeConfiguration(
5048            Account account,
5049            final Jid jid,
5050            final String node,
5051            final Bundle options,
5052            final OnConfigurationPushed callback) {
5053        Log.d(Config.LOGTAG, "pushing node configuration");
5054        sendIqPacket(
5055                account,
5056                mIqGenerator.requestPubsubConfiguration(jid, node),
5057                responseToRequest -> {
5058                    if (responseToRequest.getType() == Iq.Type.RESULT) {
5059                        Element pubsub =
5060                                responseToRequest.findChild(
5061                                        "pubsub", "http://jabber.org/protocol/pubsub#owner");
5062                        Element configuration =
5063                                pubsub == null ? null : pubsub.findChild("configure");
5064                        Element x =
5065                                configuration == null
5066                                        ? null
5067                                        : configuration.findChild("x", Namespace.DATA);
5068                        if (x != null) {
5069                            final Data data = Data.parse(x);
5070                            data.submit(options);
5071                            sendIqPacket(
5072                                    account,
5073                                    mIqGenerator.publishPubsubConfiguration(jid, node, data),
5074                                    responseToPublish -> {
5075                                        if (responseToPublish.getType() == Iq.Type.RESULT
5076                                                && callback != null) {
5077                                            Log.d(
5078                                                    Config.LOGTAG,
5079                                                    account.getJid().asBareJid()
5080                                                            + ": successfully changed node"
5081                                                            + " configuration for node "
5082                                                            + node);
5083                                            callback.onPushSucceeded();
5084                                        } else if (responseToPublish.getType() == Iq.Type.ERROR
5085                                                && callback != null) {
5086                                            callback.onPushFailed();
5087                                        }
5088                                    });
5089                        } else if (callback != null) {
5090                            callback.onPushFailed();
5091                        }
5092                    } else if (responseToRequest.getType() == Iq.Type.ERROR && callback != null) {
5093                        callback.onPushFailed();
5094                    }
5095                });
5096    }
5097
5098    public void pushConferenceConfiguration(
5099            final Conversation conversation,
5100            final Bundle options,
5101            final OnConfigurationPushed callback) {
5102        if (options.getString("muc#roomconfig_whois", "moderators").equals("anyone")) {
5103            conversation.setAttribute("accept_non_anonymous", true);
5104            updateConversation(conversation);
5105        }
5106        if (options.containsKey("muc#roomconfig_moderatedroom")) {
5107            final boolean moderated = "1".equals(options.getString("muc#roomconfig_moderatedroom"));
5108            options.putString("members_by_default", moderated ? "0" : "1");
5109        }
5110        if (options.containsKey("muc#roomconfig_allowpm")) {
5111            // ejabberd :-/
5112            final boolean allow = "anyone".equals(options.getString("muc#roomconfig_allowpm"));
5113            options.putString("allow_private_messages", allow ? "1" : "0");
5114            options.putString("allow_private_messages_from_visitors", allow ? "anyone" : "nobody");
5115        }
5116        final var account = conversation.getAccount();
5117        final Iq request = new Iq(Iq.Type.GET);
5118        request.setTo(conversation.getJid().asBareJid());
5119        request.query("http://jabber.org/protocol/muc#owner");
5120        sendIqPacket(
5121                account,
5122                request,
5123                response -> {
5124                    if (response.getType() == Iq.Type.RESULT) {
5125                        final Data data =
5126                                Data.parse(response.query().findChild("x", Namespace.DATA));
5127                        data.submit(options);
5128                        final Iq set = new Iq(Iq.Type.SET);
5129                        set.setTo(conversation.getJid().asBareJid());
5130                        set.query("http://jabber.org/protocol/muc#owner").addChild(data);
5131                        sendIqPacket(
5132                                account,
5133                                set,
5134                                packet -> {
5135                                    if (callback != null) {
5136                                        if (packet.getType() == Iq.Type.RESULT) {
5137                                            callback.onPushSucceeded();
5138                                        } else {
5139                                            Log.d(Config.LOGTAG, "failed: " + packet.toString());
5140                                            callback.onPushFailed();
5141                                        }
5142                                    }
5143                                });
5144                    } else {
5145                        if (callback != null) {
5146                            callback.onPushFailed();
5147                        }
5148                    }
5149                });
5150    }
5151
5152    public void pushSubjectToConference(final Conversation conference, final String subject) {
5153        final var packet =
5154                this.getMessageGenerator()
5155                        .conferenceSubject(conference, StringUtils.nullOnEmpty(subject));
5156        this.sendMessagePacket(conference.getAccount(), packet);
5157    }
5158
5159    public void requestVoice(final Account account, final Jid jid) {
5160        final var packet = this.getMessageGenerator().requestVoice(jid);
5161        this.sendMessagePacket(account, packet);
5162    }
5163
5164    public void changeAffiliationInConference(
5165            final Conversation conference,
5166            Jid user,
5167            final MucOptions.Affiliation affiliation,
5168            final OnAffiliationChanged callback) {
5169        final Jid jid = user.asBareJid();
5170        final Iq request =
5171                this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
5172        sendIqPacket(
5173                conference.getAccount(),
5174                request,
5175                (response) -> {
5176                    if (response.getType() == Iq.Type.RESULT) {
5177                        final var mucOptions = conference.getMucOptions();
5178                        mucOptions.changeAffiliation(jid, affiliation);
5179                        getAvatarService().clear(mucOptions);
5180                        if (callback != null) {
5181                            callback.onAffiliationChangedSuccessful(jid);
5182                        } else {
5183                            Log.d(
5184                                    Config.LOGTAG,
5185                                    "changed affiliation of " + user + " to " + affiliation);
5186                        }
5187                    } else if (callback != null) {
5188                        callback.onAffiliationChangeFailed(
5189                                jid, R.string.could_not_change_affiliation);
5190                    } else {
5191                        Log.d(Config.LOGTAG, "unable to change affiliation");
5192                    }
5193                });
5194    }
5195
5196    public void changeRoleInConference(
5197            final Conversation conference, final String nick, MucOptions.Role role) {
5198        final var account = conference.getAccount();
5199        final Iq request = this.mIqGenerator.changeRole(conference, nick, role.toString());
5200        sendIqPacket(
5201                account,
5202                request,
5203                (packet) -> {
5204                    if (packet.getType() != Iq.Type.RESULT) {
5205                        Log.d(
5206                                Config.LOGTAG,
5207                                account.getJid().asBareJid() + " unable to change role of " + nick);
5208                    }
5209                });
5210    }
5211
5212    public void moderateMessage(final Account account, final Message m, final String reason) {
5213        final var request = this.mIqGenerator.moderateMessage(account, m, reason);
5214        sendIqPacket(account, request, (packet) -> {
5215            if (packet.getType() != Iq.Type.RESULT) {
5216                showErrorToastInUi(R.string.unable_to_moderate);
5217                Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to moderate: " + packet);
5218            }
5219        });
5220    }
5221
5222    public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) {
5223        final Iq request = new Iq(Iq.Type.SET);
5224        request.setTo(conversation.getJid().asBareJid());
5225        request.query("http://jabber.org/protocol/muc#owner").addChild("destroy");
5226        sendIqPacket(
5227                conversation.getAccount(),
5228                request,
5229                response -> {
5230                    if (response.getType() == Iq.Type.RESULT) {
5231                        if (callback != null) {
5232                            callback.onRoomDestroySucceeded();
5233                        }
5234                    } else if (response.getType() == Iq.Type.ERROR) {
5235                        if (callback != null) {
5236                            callback.onRoomDestroyFailed();
5237                        }
5238                    }
5239                });
5240    }
5241
5242    private void disconnect(final Account account, boolean force) {
5243        final XmppConnection connection = account.getXmppConnection();
5244        if (connection == null) {
5245            return;
5246        }
5247        if (!force) {
5248            final List<Conversation> conversations = getConversations();
5249            for (Conversation conversation : conversations) {
5250                if (conversation.getAccount() == account) {
5251                    if (conversation.getMode() == Conversation.MODE_MULTI) {
5252                        leaveMuc(conversation, true);
5253                    }
5254                }
5255            }
5256            sendOfflinePresence(account);
5257        }
5258        connection.disconnect(force);
5259    }
5260
5261    @Override
5262    public IBinder onBind(Intent intent) {
5263        return mBinder;
5264    }
5265
5266    public void deleteMessage(Message message) {
5267        mScheduledMessages.remove(message.getUuid());
5268        databaseBackend.deleteMessage(message.getUuid());
5269        ((Conversation) message.getConversation()).remove(message);
5270        updateConversationUi();
5271    }
5272
5273    public void updateMessage(Message message) {
5274        updateMessage(message, true);
5275    }
5276
5277    public void updateMessage(Message message, boolean includeBody) {
5278        databaseBackend.updateMessage(message, includeBody);
5279        updateConversationUi();
5280    }
5281
5282    public void createMessageAsync(final Message message) {
5283        mDatabaseWriterExecutor.execute(() -> databaseBackend.createMessage(message));
5284    }
5285
5286    public void updateMessage(Message message, String uuid) {
5287        if (!databaseBackend.updateMessage(message, uuid)) {
5288            Log.e(Config.LOGTAG, "error updated message in DB after edit");
5289        }
5290        updateConversationUi();
5291    }
5292
5293    public void syncDirtyContacts(Account account) {
5294        for (Contact contact : account.getRoster().getContacts()) {
5295            if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
5296                pushContactToServer(contact);
5297            }
5298            if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
5299                deleteContactOnServer(contact);
5300            }
5301        }
5302    }
5303
5304    protected void unregisterPhoneAccounts(final Account account) {
5305        for (final Contact contact : account.getRoster().getContacts()) {
5306            if (!contact.showInRoster()) {
5307                contact.unregisterAsPhoneAccount(this);
5308            }
5309        }
5310    }
5311
5312    public void createContact(final Contact contact, final boolean autoGrant) {
5313        createContact(contact, autoGrant, null);
5314    }
5315
5316    public void createContact(
5317            final Contact contact, final boolean autoGrant, final String preAuth) {
5318        if (autoGrant) {
5319            contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
5320            contact.setOption(Contact.Options.ASKING);
5321        }
5322        pushContactToServer(contact, preAuth);
5323    }
5324
5325    public void pushContactToServer(final Contact contact) {
5326        pushContactToServer(contact, null);
5327    }
5328
5329    private void pushContactToServer(final Contact contact, final String preAuth) {
5330        contact.resetOption(Contact.Options.DIRTY_DELETE);
5331        contact.setOption(Contact.Options.DIRTY_PUSH);
5332        final Account account = contact.getAccount();
5333        if (account.getStatus() == Account.State.ONLINE) {
5334            final boolean ask = contact.getOption(Contact.Options.ASKING);
5335            final boolean sendUpdates =
5336                    contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
5337                            && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
5338            final Iq iq = new Iq(Iq.Type.SET);
5339            iq.query(Namespace.ROSTER).addChild(contact.asElement());
5340            account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
5341            if (sendUpdates) {
5342                sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
5343            }
5344            if (ask) {
5345                sendPresencePacket(
5346                        account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth));
5347            }
5348        } else {
5349            syncRoster(contact.getAccount());
5350        }
5351    }
5352
5353    public void publishMucAvatar(
5354            final Conversation conversation, final Uri image, final OnAvatarPublication callback) {
5355        new Thread(
5356                        () -> {
5357                            final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
5358                            final int size = Config.AVATAR_SIZE;
5359                            final Avatar avatar =
5360                                    getFileBackend().getPepAvatar(image, size, format);
5361                            if (avatar != null) {
5362                                if (!getFileBackend().save(avatar)) {
5363                                    callback.onAvatarPublicationFailed(
5364                                            R.string.error_saving_avatar);
5365                                    return;
5366                                }
5367                                avatar.owner = conversation.getJid().asBareJid();
5368                                publishMucAvatar(conversation, avatar, callback);
5369                            } else {
5370                                callback.onAvatarPublicationFailed(
5371                                        R.string.error_publish_avatar_converting);
5372                            }
5373                        })
5374                .start();
5375    }
5376
5377    public void publishAvatarAsync(
5378            final Account account,
5379            final Uri image,
5380            final boolean open,
5381            final OnAvatarPublication callback) {
5382        new Thread(() -> publishAvatar(account, image, open, callback)).start();
5383    }
5384
5385    private void publishAvatar(
5386            final Account account,
5387            final Uri image,
5388            final boolean open,
5389            final OnAvatarPublication callback) {
5390        final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
5391        final int size = Config.AVATAR_SIZE;
5392        final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
5393        if (avatar != null) {
5394            if (!getFileBackend().save(avatar)) {
5395                Log.d(Config.LOGTAG, "unable to save vcard");
5396                callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
5397                return;
5398            }
5399            publishAvatar(account, avatar, open, callback);
5400        } else {
5401            callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
5402        }
5403    }
5404
5405    private void publishMucAvatar(
5406            Conversation conversation, Avatar avatar, OnAvatarPublication callback) {
5407        final var account = conversation.getAccount();
5408        final Iq retrieve = mIqGenerator.retrieveVcardAvatar(avatar);
5409        sendIqPacket(
5410                account,
5411                retrieve,
5412                (response) -> {
5413                    boolean itemNotFound =
5414                            response.getType() == Iq.Type.ERROR
5415                                    && response.hasChild("error")
5416                                    && response.findChild("error").hasChild("item-not-found");
5417                    if (response.getType() == Iq.Type.RESULT || itemNotFound) {
5418                        Element vcard = response.findChild("vCard", "vcard-temp");
5419                        if (vcard == null) {
5420                            vcard = new Element("vCard", "vcard-temp");
5421                        }
5422                        Element photo = vcard.findChild("PHOTO");
5423                        if (photo == null) {
5424                            photo = vcard.addChild("PHOTO");
5425                        }
5426                        photo.clearChildren();
5427                        photo.addChild("TYPE").setContent(avatar.type);
5428                        photo.addChild("BINVAL").setContent(avatar.image);
5429                        final Iq publication = new Iq(Iq.Type.SET);
5430                        publication.setTo(conversation.getJid().asBareJid());
5431                        publication.addChild(vcard);
5432                        sendIqPacket(
5433                                account,
5434                                publication,
5435                                (publicationResponse) -> {
5436                                    if (publicationResponse.getType() == Iq.Type.RESULT) {
5437                                        callback.onAvatarPublicationSucceeded();
5438                                    } else {
5439                                        Log.d(
5440                                                Config.LOGTAG,
5441                                                "failed to publish vcard "
5442                                                        + publicationResponse.getErrorCondition());
5443                                        callback.onAvatarPublicationFailed(
5444                                                R.string.error_publish_avatar_server_reject);
5445                                    }
5446                                });
5447                    } else {
5448                        Log.d(Config.LOGTAG, "failed to request vcard " + response);
5449                        callback.onAvatarPublicationFailed(
5450                                R.string.error_publish_avatar_no_server_support);
5451                    }
5452                });
5453    }
5454
5455    public void publishAvatar(
5456            final Account account,
5457            final Avatar avatar,
5458            final boolean open,
5459            final OnAvatarPublication callback) {
5460        final Bundle options;
5461        if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
5462            options = open ? PublishOptions.openAccess() : PublishOptions.presenceAccess();
5463        } else {
5464            options = null;
5465        }
5466        publishAvatar(account, avatar, options, true, callback);
5467    }
5468
5469    public void publishAvatar(
5470            Account account,
5471            final Avatar avatar,
5472            final Bundle options,
5473            final boolean retry,
5474            final OnAvatarPublication callback) {
5475        Log.d(
5476                Config.LOGTAG,
5477                account.getJid().asBareJid() + ": publishing avatar. options=" + options);
5478        final Iq packet = this.mIqGenerator.publishAvatar(avatar, options);
5479        this.sendIqPacket(
5480                account,
5481                packet,
5482                result -> {
5483                    if (result.getType() == Iq.Type.RESULT) {
5484                        publishAvatarMetadata(account, avatar, options, true, callback);
5485                    } else if (retry && PublishOptions.preconditionNotMet(result)) {
5486                        pushNodeConfiguration(
5487                                account,
5488                                Namespace.AVATAR_DATA,
5489                                options,
5490                                new OnConfigurationPushed() {
5491                                    @Override
5492                                    public void onPushSucceeded() {
5493                                        Log.d(
5494                                                Config.LOGTAG,
5495                                                account.getJid().asBareJid()
5496                                                        + ": changed node configuration for avatar"
5497                                                        + " node");
5498                                        publishAvatar(account, avatar, options, false, callback);
5499                                    }
5500
5501                                    @Override
5502                                    public void onPushFailed() {
5503                                        Log.d(
5504                                                Config.LOGTAG,
5505                                                account.getJid().asBareJid()
5506                                                        + ": unable to change node configuration"
5507                                                        + " for avatar node");
5508                                        publishAvatar(account, avatar, null, false, callback);
5509                                    }
5510                                });
5511                    } else {
5512                        Element error = result.findChild("error");
5513                        Log.d(
5514                                Config.LOGTAG,
5515                                account.getJid().asBareJid()
5516                                        + ": server rejected avatar "
5517                                        + (avatar.size / 1024)
5518                                        + "KiB "
5519                                        + (error != null ? error.toString() : ""));
5520                        if (callback != null) {
5521                            callback.onAvatarPublicationFailed(
5522                                    R.string.error_publish_avatar_server_reject);
5523                        }
5524                    }
5525                });
5526    }
5527
5528    public void publishAvatarMetadata(
5529            Account account,
5530            final Avatar avatar,
5531            final Bundle options,
5532            final boolean retry,
5533            final OnAvatarPublication callback) {
5534        final Iq packet =
5535                XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options);
5536        sendIqPacket(
5537                account,
5538                packet,
5539                result -> {
5540                    if (result.getType() == Iq.Type.RESULT) {
5541                        if (account.setAvatar(avatar.getFilename())) {
5542                            getAvatarService().clear(account);
5543                            databaseBackend.updateAccount(account);
5544                            notifyAccountAvatarHasChanged(account);
5545                        }
5546                        Log.d(
5547                                Config.LOGTAG,
5548                                account.getJid().asBareJid()
5549                                        + ": published avatar "
5550                                        + (avatar.size / 1024)
5551                                        + "KiB");
5552                        if (callback != null) {
5553                            callback.onAvatarPublicationSucceeded();
5554                        }
5555                    } else if (retry && PublishOptions.preconditionNotMet(result)) {
5556                        pushNodeConfiguration(
5557                                account,
5558                                Namespace.AVATAR_METADATA,
5559                                options,
5560                                new OnConfigurationPushed() {
5561                                    @Override
5562                                    public void onPushSucceeded() {
5563                                        Log.d(
5564                                                Config.LOGTAG,
5565                                                account.getJid().asBareJid()
5566                                                        + ": changed node configuration for avatar"
5567                                                        + " meta data node");
5568                                        publishAvatarMetadata(
5569                                                account, avatar, options, false, callback);
5570                                    }
5571
5572                                    @Override
5573                                    public void onPushFailed() {
5574                                        Log.d(
5575                                                Config.LOGTAG,
5576                                                account.getJid().asBareJid()
5577                                                        + ": unable to change node configuration"
5578                                                        + " for avatar meta data node");
5579                                        publishAvatarMetadata(
5580                                                account, avatar, null, false, callback);
5581                                    }
5582                                });
5583                    } else {
5584                        if (callback != null) {
5585                            callback.onAvatarPublicationFailed(
5586                                    R.string.error_publish_avatar_server_reject);
5587                        }
5588                    }
5589                });
5590    }
5591
5592    public void republishAvatarIfNeeded(final Account account) {
5593        if (account.getAxolotlService().isPepBroken()) {
5594            Log.d(
5595                    Config.LOGTAG,
5596                    account.getJid().asBareJid()
5597                            + ": skipping republication of avatar because pep is broken");
5598            return;
5599        }
5600        final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
5601        this.sendIqPacket(
5602                account,
5603                packet,
5604                new Consumer<Iq>() {
5605
5606                    private Avatar parseAvatar(final Iq packet) {
5607                        final var pubsub = packet.getExtension(PubSub.class);
5608                        if (pubsub == null) {
5609                            return null;
5610                        }
5611                        final var items = pubsub.getItems();
5612                        if (items == null) {
5613                            return null;
5614                        }
5615                        final var item = items.getFirstItemWithId(Metadata.class);
5616                        if (item == null) {
5617                            return null;
5618                        }
5619                        return Avatar.parseMetadata(item.getKey(), item.getValue());
5620                    }
5621
5622                    private boolean errorIsItemNotFound(Iq packet) {
5623                        Element error = packet.findChild("error");
5624                        return packet.getType() == Iq.Type.ERROR
5625                                && error != null
5626                                && error.hasChild("item-not-found");
5627                    }
5628
5629                    @Override
5630                    public void accept(final Iq packet) {
5631                        if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) {
5632                            final Avatar serverAvatar = parseAvatar(packet);
5633                            if (serverAvatar == null && account.getAvatar() != null) {
5634                                final Avatar avatar =
5635                                        fileBackend.getStoredPepAvatar(account.getAvatar());
5636                                if (avatar != null) {
5637                                    Log.d(
5638                                            Config.LOGTAG,
5639                                            account.getJid().asBareJid()
5640                                                    + ": avatar on server was null. republishing");
5641                                    // publishing as 'open' - old server (that requires
5642                                    // republication) likely doesn't support access models anyway
5643                                    publishAvatar(
5644                                            account,
5645                                            fileBackend.getStoredPepAvatar(account.getAvatar()),
5646                                            true,
5647                                            null);
5648                                } else {
5649                                    Log.e(
5650                                            Config.LOGTAG,
5651                                            account.getJid().asBareJid()
5652                                                    + ": error rereading avatar");
5653                                }
5654                            }
5655                        }
5656                    }
5657                });
5658    }
5659
5660    public void cancelAvatarFetches(final Account account) {
5661        synchronized (mInProgressAvatarFetches) {
5662            for (final Iterator<String> iterator = mInProgressAvatarFetches.iterator();
5663                    iterator.hasNext(); ) {
5664                final String KEY = iterator.next();
5665                if (KEY.startsWith(account.getJid().asBareJid() + "_")) {
5666                    iterator.remove();
5667                }
5668            }
5669        }
5670    }
5671
5672    public void fetchAvatar(Account account, Avatar avatar) {
5673        fetchAvatar(account, avatar, null);
5674    }
5675
5676    public void fetchAvatar(
5677            Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5678        if (databaseBackend.isBlockedMedia(avatar.cid())) {
5679            if (callback != null) callback.error(0, null);
5680            return;
5681        }
5682
5683        final String KEY = generateFetchKey(account, avatar);
5684        synchronized (this.mInProgressAvatarFetches) {
5685            if (mInProgressAvatarFetches.add(KEY)) {
5686                switch (avatar.origin) {
5687                    case PEP:
5688                        this.mInProgressAvatarFetches.add(KEY);
5689                        fetchAvatarPep(account, avatar, callback);
5690                        break;
5691                    case VCARD:
5692                        this.mInProgressAvatarFetches.add(KEY);
5693                        fetchAvatarVcard(account, avatar, callback);
5694                        break;
5695                }
5696            } else if (avatar.origin == Avatar.Origin.PEP) {
5697                mOmittedPepAvatarFetches.add(KEY);
5698            } else {
5699                Log.d(
5700                        Config.LOGTAG,
5701                        account.getJid().asBareJid()
5702                                + ": already fetching "
5703                                + avatar.origin
5704                                + " avatar for "
5705                                + avatar.owner);
5706            }
5707        }
5708    }
5709
5710    private void fetchAvatarPep(
5711            final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5712        final Iq packet = this.mIqGenerator.retrievePepAvatar(avatar);
5713        sendIqPacket(
5714                account,
5715                packet,
5716                (result) -> {
5717                    synchronized (mInProgressAvatarFetches) {
5718                        mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
5719                    }
5720                    final String ERROR =
5721                            account.getJid().asBareJid()
5722                                    + ": fetching avatar for "
5723                                    + avatar.owner
5724                                    + " failed ";
5725                    if (result.getType() == Iq.Type.RESULT) {
5726                        avatar.image = IqParser.avatarData(result);
5727                        if (avatar.image != null) {
5728                            if (getFileBackend().save(avatar)) {
5729                                if (account.getJid().asBareJid().equals(avatar.owner)) {
5730                                    if (account.setAvatar(avatar.getFilename())) {
5731                                        databaseBackend.updateAccount(account);
5732                                    }
5733                                    getAvatarService().clear(account);
5734                                    updateConversationUi();
5735                                    updateAccountUi();
5736                                } else {
5737                                    final Contact contact =
5738                                            account.getRoster().getContact(avatar.owner);
5739                                    contact.setAvatar(avatar);
5740                                    syncRoster(account);
5741                                    getAvatarService().clear(contact);
5742                                    updateConversationUi();
5743                                    updateRosterUi(UpdateRosterReason.AVATAR);
5744                                }
5745                                if (callback != null) {
5746                                    callback.success(avatar);
5747                                }
5748                                Log.d(
5749                                        Config.LOGTAG,
5750                                        account.getJid().asBareJid()
5751                                                + ": successfully fetched pep avatar for "
5752                                                + avatar.owner);
5753                                return;
5754                            }
5755                        } else {
5756
5757                            Log.d(Config.LOGTAG, ERROR + "(parsing error)");
5758                        }
5759                    } else {
5760                        Element error = result.findChild("error");
5761                        if (error == null) {
5762                            Log.d(Config.LOGTAG, ERROR + "(server error)");
5763                        } else {
5764                            Log.d(Config.LOGTAG, ERROR + error.toString());
5765                        }
5766                    }
5767                    if (callback != null) {
5768                        callback.error(0, null);
5769                    }
5770                });
5771    }
5772
5773    private void fetchAvatarVcard(
5774            final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5775        final Iq packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
5776        this.sendIqPacket(
5777                account,
5778                packet,
5779                response -> {
5780                    final boolean previouslyOmittedPepFetch;
5781                    synchronized (mInProgressAvatarFetches) {
5782                        final String KEY = generateFetchKey(account, avatar);
5783                        mInProgressAvatarFetches.remove(KEY);
5784                        previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY);
5785                    }
5786                    if (response.getType() == Iq.Type.RESULT) {
5787                        Element vCard = response.findChild("vCard", "vcard-temp");
5788                        Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
5789                        String image = photo != null ? photo.findChildContent("BINVAL") : null;
5790                        if (image != null) {
5791                            avatar.image = image;
5792                            if (getFileBackend().save(avatar)) {
5793                                Log.d(
5794                                        Config.LOGTAG,
5795                                        account.getJid().asBareJid()
5796                                                + ": successfully fetched vCard avatar for "
5797                                                + avatar.owner
5798                                                + " omittedPep="
5799                                                + previouslyOmittedPepFetch);
5800                                if (avatar.owner.isBareJid()) {
5801                                    if (account.getJid().asBareJid().equals(avatar.owner)
5802                                            && account.getAvatar() == null) {
5803                                        Log.d(
5804                                                Config.LOGTAG,
5805                                                account.getJid().asBareJid()
5806                                                        + ": had no avatar. replacing with vcard");
5807                                        account.setAvatar(avatar.getFilename());
5808                                        databaseBackend.updateAccount(account);
5809                                        getAvatarService().clear(account);
5810                                        updateAccountUi();
5811                                    } else {
5812                                        final Contact contact =
5813                                                account.getRoster().getContact(avatar.owner);
5814                                        contact.setAvatar(avatar, previouslyOmittedPepFetch);
5815                                        syncRoster(account);
5816                                        getAvatarService().clear(contact);
5817                                        updateRosterUi(UpdateRosterReason.AVATAR);
5818                                    }
5819                                    updateConversationUi();
5820                                } else {
5821                                    Conversation conversation =
5822                                            find(account, avatar.owner.asBareJid());
5823                                    if (conversation != null
5824                                            && conversation.getMode() == Conversation.MODE_MULTI) {
5825                                        MucOptions.User user =
5826                                                conversation
5827                                                        .getMucOptions()
5828                                                        .findUserByFullJid(avatar.owner);
5829                                        if (user != null) {
5830                                            if (user.setAvatar(avatar)) {
5831                                                getAvatarService().clear(user);
5832                                                updateConversationUi();
5833                                                updateMucRosterUi();
5834                                            }
5835                                            if (user.getRealJid() != null) {
5836                                                Contact contact =
5837                                                        account.getRoster()
5838                                                                .getContact(user.getRealJid());
5839                                                contact.setAvatar(avatar);
5840                                                syncRoster(account);
5841                                                getAvatarService().clear(contact);
5842                                                updateRosterUi(UpdateRosterReason.AVATAR);
5843                                            }
5844                                        }
5845                                    }
5846                                }
5847                            }
5848                        }
5849                    }
5850                });
5851    }
5852
5853    public void checkForAvatar(final Account account, final UiCallback<Avatar> callback) {
5854        final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
5855        this.sendIqPacket(
5856                account,
5857                packet,
5858                response -> {
5859                    if (response.getType() != Iq.Type.RESULT) {
5860                        callback.error(0, null);
5861                    }
5862                    final var pubsub = packet.getExtension(PubSub.class);
5863                    if (pubsub == null) {
5864                        callback.error(0, null);
5865                        return;
5866                    }
5867                    final var items = pubsub.getItems();
5868                    if (items == null) {
5869                        callback.error(0, null);
5870                        return;
5871                    }
5872                    final var item = items.getFirstItemWithId(Metadata.class);
5873                    if (item == null) {
5874                        callback.error(0, null);
5875                        return;
5876                    }
5877                    final var avatar = Avatar.parseMetadata(item.getKey(), item.getValue());
5878                    if (avatar == null) {
5879                        callback.error(0, null);
5880                        return;
5881                    }
5882                    avatar.owner = account.getJid().asBareJid();
5883                    if (fileBackend.isAvatarCached(avatar)) {
5884                        if (account.setAvatar(avatar.getFilename())) {
5885                            databaseBackend.updateAccount(account);
5886                        }
5887                        getAvatarService().clear(account);
5888                        callback.success(avatar);
5889                    } else {
5890                        fetchAvatarPep(account, avatar, callback);
5891                    }
5892                });
5893    }
5894
5895    public void notifyAccountAvatarHasChanged(final Account account) {
5896        final XmppConnection connection = account.getXmppConnection();
5897        if (connection != null && connection.getFeatures().bookmarksConversion()) {
5898            Log.d(
5899                    Config.LOGTAG,
5900                    account.getJid().asBareJid()
5901                            + ": avatar changed. resending presence to online group chats");
5902            for (Conversation conversation : conversations) {
5903                if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) {
5904                    presenceToMuc(conversation);
5905                }
5906            }
5907        }
5908    }
5909
5910    public void fetchVcard4(Account account, final Contact contact, final Consumer<Element> callback) {
5911        final var packet = this.mIqGenerator.retrieveVcard4(contact.getJid());
5912        sendIqPacket(account, packet, (result) -> {
5913            if (result.getType() == Iq.Type.RESULT) {
5914                final Element item = IqParser.getItem(result);
5915                if (item != null) {
5916                    final Element vcard4 = item.findChild("vcard", Namespace.VCARD4);
5917                    if (vcard4 != null) {
5918                        if (callback != null) {
5919                            callback.accept(vcard4);
5920                        }
5921                        return;
5922                    }
5923                }
5924            } else {
5925                Element error = result.findChild("error");
5926                if (error == null) {
5927                    Log.d(Config.LOGTAG, "fetchVcard4 (server error)");
5928                } else {
5929                    Log.d(Config.LOGTAG, "fetchVcard4 " + error.toString());
5930                }
5931            }
5932            if (callback != null) {
5933                callback.accept(null);
5934            }
5935
5936        });
5937    }
5938
5939    public void deleteContactOnServer(Contact contact) {
5940        contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
5941        contact.resetOption(Contact.Options.DIRTY_PUSH);
5942        contact.setOption(Contact.Options.DIRTY_DELETE);
5943        Account account = contact.getAccount();
5944        if (account.getStatus() == Account.State.ONLINE) {
5945            final Iq iq = new Iq(Iq.Type.SET);
5946            Element item = iq.query(Namespace.ROSTER).addChild("item");
5947            item.setAttribute("jid", contact.getJid());
5948            item.setAttribute("subscription", "remove");
5949            account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
5950        }
5951    }
5952
5953    public void updateConversation(final Conversation conversation) {
5954        mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation));
5955    }
5956
5957    private void reconnectAccount(
5958            final Account account, final boolean force, final boolean interactive) {
5959        synchronized (account) {
5960            final XmppConnection existingConnection = account.getXmppConnection();
5961            final XmppConnection connection;
5962            if (existingConnection != null) {
5963                connection = existingConnection;
5964            } else if (account.isConnectionEnabled()) {
5965                connection = createConnection(account);
5966                account.setXmppConnection(connection);
5967            } else {
5968                return;
5969            }
5970            final boolean hasInternet = hasInternetConnection();
5971            if (account.isConnectionEnabled() && hasInternet) {
5972                if (!force) {
5973                    disconnect(account, false);
5974                }
5975                Thread thread = new Thread(connection);
5976                connection.setInteractive(interactive);
5977                connection.prepareNewConnection();
5978                connection.interrupt();
5979                thread.start();
5980                scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
5981            } else {
5982                disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
5983                account.getRoster().clearPresences();
5984                connection.resetEverything();
5985                final AxolotlService axolotlService = account.getAxolotlService();
5986                if (axolotlService != null) {
5987                    axolotlService.resetBrokenness();
5988                }
5989                if (!hasInternet) {
5990                    account.setStatus(Account.State.NO_INTERNET);
5991                }
5992            }
5993        }
5994    }
5995
5996    public void reconnectAccountInBackground(final Account account) {
5997        new Thread(() -> reconnectAccount(account, false, true)).start();
5998    }
5999
6000    public void invite(final Conversation conversation, final Jid contact) {
6001        Log.d(
6002                Config.LOGTAG,
6003                conversation.getAccount().getJid().asBareJid()
6004                        + ": inviting "
6005                        + contact
6006                        + " to "
6007                        + conversation.getJid().asBareJid());
6008        final MucOptions.User user =
6009                conversation.getMucOptions().findUserByRealJid(contact.asBareJid());
6010        if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) {
6011            changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null);
6012        }
6013        final var packet = mMessageGenerator.invite(conversation, contact);
6014        sendMessagePacket(conversation.getAccount(), packet);
6015    }
6016
6017    public void directInvite(Conversation conversation, Jid jid) {
6018        final var packet = mMessageGenerator.directInvite(conversation, jid);
6019        sendMessagePacket(conversation.getAccount(), packet);
6020    }
6021
6022    public void resetSendingToWaiting(Account account) {
6023        for (Conversation conversation : getConversations()) {
6024            if (conversation.getAccount() == account) {
6025                conversation.findUnsentTextMessages(
6026                        message -> markMessage(message, Message.STATUS_WAITING));
6027            }
6028        }
6029    }
6030
6031    public Message markMessage(
6032            final Account account, final Jid recipient, final String uuid, final int status) {
6033        return markMessage(account, recipient, uuid, status, null);
6034    }
6035
6036    public Message markMessage(
6037            final Account account,
6038            final Jid recipient,
6039            final String uuid,
6040            final int status,
6041            String errorMessage) {
6042        if (uuid == null) {
6043            return null;
6044        }
6045        for (Conversation conversation : getConversations()) {
6046            if (conversation.getJid().asBareJid().equals(recipient)
6047                    && conversation.getAccount() == account) {
6048                final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
6049                if (message != null) {
6050                    markMessage(message, status, errorMessage);
6051                }
6052                return message;
6053            }
6054        }
6055        return null;
6056    }
6057
6058    public boolean markMessage(
6059            final Conversation conversation,
6060            final String uuid,
6061            final int status,
6062            final String serverMessageId) {
6063        return markMessage(conversation, uuid, status, serverMessageId, null, null, null, null, null);
6064    }
6065
6066    public boolean markMessage(final Conversation conversation, final String uuid, final int status, final String serverMessageId, final LocalizedContent body, final Element html, final String subject, final Element thread, final Set<Message.FileParams> attachments) {
6067        if (uuid == null) {
6068            return false;
6069        } else {
6070            final Message message = conversation.findSentMessageWithUuid(uuid);
6071            if (message != null) {
6072                if (message.getServerMsgId() == null) {
6073                    message.setServerMsgId(serverMessageId);
6074                }
6075                if (message.getEncryption() == Message.ENCRYPTION_NONE && (body != null || html != null || subject != null || thread != null || attachments != null)) {
6076                    message.setBody(body.content);
6077                    if (body.count > 1) {
6078                        message.setBodyLanguage(body.language);
6079                    }
6080                    message.setHtml(html);
6081                    message.setSubject(subject);
6082                    message.setThread(thread);
6083                    if (attachments != null && attachments.isEmpty()) {
6084                        message.setRelativeFilePath(null);
6085                        message.resetFileParams();
6086                    }
6087                    markMessage(message, status, null, true);
6088                } else {
6089                    markMessage(message, status);
6090                }
6091                return true;
6092            } else {
6093                return false;
6094            }
6095        }
6096    }
6097
6098    public void markMessage(Message message, int status) {
6099        markMessage(message, status, null);
6100    }
6101
6102    public void markMessage(final Message message, final int status, final String errorMessage) {
6103        markMessage(message, status, errorMessage, false);
6104    }
6105
6106    public void markMessage(
6107            final Message message,
6108            final int status,
6109            final String errorMessage,
6110            final boolean includeBody) {
6111        final int oldStatus = message.getStatus();
6112        if (status == Message.STATUS_SEND_FAILED
6113                && (oldStatus == Message.STATUS_SEND_RECEIVED
6114                        || oldStatus == Message.STATUS_SEND_DISPLAYED)) {
6115            return;
6116        }
6117        if (status == Message.STATUS_SEND_RECEIVED && oldStatus == Message.STATUS_SEND_DISPLAYED) {
6118            return;
6119        }
6120        message.setErrorMessage(errorMessage);
6121        message.setStatus(status);
6122        databaseBackend.updateMessage(message, includeBody);
6123        updateConversationUi();
6124        if (oldStatus != status && status == Message.STATUS_SEND_FAILED) {
6125            mNotificationService.pushFailedDelivery(message);
6126        }
6127    }
6128
6129    public SharedPreferences getPreferences() {
6130        return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
6131    }
6132
6133    public long getAutomaticMessageDeletionDate() {
6134        final long timeout =
6135                getLongPreference(
6136                        AppSettings.AUTOMATIC_MESSAGE_DELETION,
6137                        R.integer.automatic_message_deletion);
6138        return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000));
6139    }
6140
6141    public long getLongPreference(String name, @IntegerRes int res) {
6142        long defaultValue = getResources().getInteger(res);
6143        try {
6144            return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue)));
6145        } catch (NumberFormatException e) {
6146            return defaultValue;
6147        }
6148    }
6149
6150    public boolean getBooleanPreference(String name, @BoolRes int res) {
6151        return getPreferences().getBoolean(name, getResources().getBoolean(res));
6152    }
6153
6154    public String getStringPreference(String name, @BoolRes int res) {
6155        return getPreferences().getString(name, getResources().getString(res));
6156    }
6157
6158    public boolean confirmMessages() {
6159        return getBooleanPreference("confirm_messages", R.bool.confirm_messages);
6160    }
6161
6162    public boolean allowMessageCorrection() {
6163        return getBooleanPreference("allow_message_correction", R.bool.allow_message_correction);
6164    }
6165
6166    public boolean sendChatStates() {
6167        return getBooleanPreference("chat_states", R.bool.chat_states);
6168    }
6169
6170    public boolean useTorToConnect() {
6171        return getBooleanPreference("use_tor", R.bool.use_tor);
6172    }
6173
6174    public boolean broadcastLastActivity() {
6175        return getBooleanPreference(AppSettings.BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
6176    }
6177
6178    public int unreadCount() {
6179        int count = 0;
6180        for (Conversation conversation : getConversations()) {
6181            count += conversation.unreadCount(this);
6182        }
6183        return count;
6184    }
6185
6186    private <T> List<T> threadSafeList(Set<T> set) {
6187        synchronized (LISTENER_LOCK) {
6188            return set.isEmpty() ? Collections.emptyList() : new ArrayList<>(set);
6189        }
6190    }
6191
6192    public void showErrorToastInUi(int resId) {
6193        for (OnShowErrorToast listener : threadSafeList(this.mOnShowErrorToasts)) {
6194            listener.onShowErrorToast(resId);
6195        }
6196    }
6197
6198    public void updateConversationUi() {
6199        updateConversationUi(false);
6200    }
6201
6202    public void updateConversationUi(boolean newCaps) {
6203        for (OnConversationUpdate listener : threadSafeList(this.mOnConversationUpdates)) {
6204            listener.onConversationUpdate(newCaps);
6205        }
6206    }
6207
6208    public void notifyJingleRtpConnectionUpdate(
6209            final Account account,
6210            final Jid with,
6211            final String sessionId,
6212            final RtpEndUserState state) {
6213        for (OnJingleRtpConnectionUpdate listener :
6214                threadSafeList(this.onJingleRtpConnectionUpdate)) {
6215            listener.onJingleRtpConnectionUpdate(account, with, sessionId, state);
6216        }
6217    }
6218
6219    public void notifyJingleRtpConnectionUpdate(
6220            CallIntegration.AudioDevice selectedAudioDevice,
6221            Set<CallIntegration.AudioDevice> availableAudioDevices) {
6222        for (OnJingleRtpConnectionUpdate listener :
6223                threadSafeList(this.onJingleRtpConnectionUpdate)) {
6224            listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
6225        }
6226    }
6227
6228    public void updateAccountUi() {
6229        for (final OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) {
6230            listener.onAccountUpdate();
6231        }
6232    }
6233
6234    public void updateRosterUi(final UpdateRosterReason reason) {
6235        if (reason == UpdateRosterReason.PRESENCE) throw new IllegalArgumentException("PRESENCE must also come with a contact");
6236        updateRosterUi(reason, null);
6237    }
6238
6239    public void updateRosterUi(final UpdateRosterReason reason, final Contact contact) {
6240        for (OnRosterUpdate listener : threadSafeList(this.mOnRosterUpdates)) {
6241            listener.onRosterUpdate(reason, contact);
6242        }
6243    }
6244
6245    public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
6246        if (mOnCaptchaRequested.size() > 0) {
6247            DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
6248            Bitmap scaled =
6249                    Bitmap.createScaledBitmap(
6250                            captcha,
6251                            (int) (captcha.getWidth() * metrics.scaledDensity),
6252                            (int) (captcha.getHeight() * metrics.scaledDensity),
6253                            false);
6254            for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
6255                listener.onCaptchaRequested(account, id, data, scaled);
6256            }
6257            return true;
6258        }
6259        return false;
6260    }
6261
6262    public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
6263        for (OnUpdateBlocklist listener : threadSafeList(this.mOnUpdateBlocklist)) {
6264            listener.OnUpdateBlocklist(status);
6265        }
6266    }
6267
6268    public void updateMucRosterUi() {
6269        for (OnMucRosterUpdate listener : threadSafeList(this.mOnMucRosterUpdate)) {
6270            listener.onMucRosterUpdate();
6271        }
6272    }
6273
6274    public void keyStatusUpdated(AxolotlService.FetchStatus report) {
6275        for (OnKeyStatusUpdated listener : threadSafeList(this.mOnKeyStatusUpdated)) {
6276            listener.onKeyStatusUpdated(report);
6277        }
6278    }
6279
6280    public Account findAccountByJid(final Jid jid) {
6281        for (final Account account : this.accounts) {
6282            if (account.getJid().asBareJid().equals(jid.asBareJid())) {
6283                return account;
6284            }
6285        }
6286        return null;
6287    }
6288
6289    public Account findAccountByUuid(final String uuid) {
6290        for (Account account : this.accounts) {
6291            if (account.getUuid().equals(uuid)) {
6292                return account;
6293            }
6294        }
6295        return null;
6296    }
6297
6298    public Conversation findConversationByUuid(String uuid) {
6299        for (Conversation conversation : getConversations()) {
6300            if (conversation.getUuid().equals(uuid)) {
6301                return conversation;
6302            }
6303        }
6304        return null;
6305    }
6306
6307    public Conversation findUniqueConversationByJid(XmppUri xmppUri) {
6308        List<Conversation> findings = new ArrayList<>();
6309        for (Conversation c : getConversations()) {
6310            if (c.getAccount().isEnabled()
6311                    && c.getJid().asBareJid().equals(xmppUri.getJid().asBareJid())
6312                    && ((c.getMode() == Conversational.MODE_MULTI)
6313                            == xmppUri.isAction(XmppUri.ACTION_JOIN))) {
6314                findings.add(c);
6315            }
6316        }
6317        return findings.size() == 1 ? findings.get(0) : null;
6318    }
6319
6320    public boolean markRead(final Conversation conversation, boolean dismiss) {
6321        return markRead(conversation, null, dismiss).size() > 0;
6322    }
6323
6324    public void markRead(final Conversation conversation) {
6325        markRead(conversation, null, true);
6326    }
6327
6328    public List<Message> markRead(
6329            final Conversation conversation, String upToUuid, boolean dismiss) {
6330        if (dismiss) {
6331            mNotificationService.clear(conversation);
6332        }
6333        final List<Message> readMessages = conversation.markRead(upToUuid);
6334        if (readMessages.size() > 0) {
6335            Runnable runnable =
6336                    () -> {
6337                        for (Message message : readMessages) {
6338                            databaseBackend.updateMessage(message, false);
6339                        }
6340                    };
6341            mDatabaseWriterExecutor.execute(runnable);
6342            updateConversationUi();
6343            updateUnreadCountBadge();
6344            return readMessages;
6345        } else {
6346            return readMessages;
6347        }
6348    }
6349
6350    public void markNotificationDismissed(final List<Message> messages) {
6351        Runnable runnable = () -> {
6352            for (final var message : messages) {
6353                message.markNotificationDismissed();
6354                databaseBackend.updateMessage(message, false);
6355            }
6356        };
6357        mDatabaseWriterExecutor.execute(runnable);
6358    }
6359
6360    public synchronized void updateUnreadCountBadge() {
6361        int count = unreadCount();
6362        if (unreadCount != count) {
6363            Log.d(Config.LOGTAG, "update unread count to " + count);
6364            if (count > 0) {
6365                ShortcutBadger.applyCount(getApplicationContext(), count);
6366            } else {
6367                ShortcutBadger.removeCount(getApplicationContext());
6368            }
6369            unreadCount = count;
6370        }
6371    }
6372
6373    public void sendReadMarker(final Conversation conversation, final String upToUuid) {
6374        final boolean isPrivateAndNonAnonymousMuc =
6375                conversation.getMode() == Conversation.MODE_MULTI
6376                        && conversation.isPrivateAndNonAnonymous();
6377        final List<Message> readMessages = this.markRead(conversation, upToUuid, true);
6378        if (readMessages.isEmpty()) {
6379            return;
6380        }
6381        final var account = conversation.getAccount();
6382        final var connection = account.getXmppConnection();
6383        updateConversationUi();
6384        final var last =
6385                Iterables.getLast(
6386                        Collections2.filter(
6387                                readMessages,
6388                                m ->
6389                                        !m.isPrivateMessage()
6390                                                && m.getStatus() == Message.STATUS_RECEIVED),
6391                        null);
6392        if (last == null) {
6393            return;
6394        }
6395
6396        final boolean sendDisplayedMarker =
6397                confirmMessages()
6398                        && (last.trusted() || isPrivateAndNonAnonymousMuc)
6399                        && last.getRemoteMsgId() != null
6400                        && (last.markable || isPrivateAndNonAnonymousMuc);
6401        final boolean serverAssist =
6402                connection != null && connection.getFeatures().mdsServerAssist();
6403
6404        final String stanzaId = last.getServerMsgId();
6405
6406        if (sendDisplayedMarker && serverAssist) {
6407            final var mdsDisplayed = mIqGenerator.mdsDisplayed(stanzaId, conversation);
6408            final var packet = mMessageGenerator.confirm(last);
6409            packet.addChild(mdsDisplayed);
6410            if (!last.isPrivateMessage()) {
6411                packet.setTo(packet.getTo().asBareJid());
6412            }
6413            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server assisted " + packet);
6414            this.sendMessagePacket(account, packet);
6415        } else {
6416            publishMds(last);
6417            // read markers will be sent after MDS to flush the CSI stanza queue
6418            if (sendDisplayedMarker) {
6419                Log.d(
6420                        Config.LOGTAG,
6421                        conversation.getAccount().getJid().asBareJid()
6422                                + ": sending displayed marker to "
6423                                + last.getCounterpart().toString());
6424                final var packet = mMessageGenerator.confirm(last);
6425                this.sendMessagePacket(account, packet);
6426            }
6427        }
6428    }
6429
6430    private void publishMds(@Nullable final Message message) {
6431        final String stanzaId = message == null ? null : message.getServerMsgId();
6432        if (Strings.isNullOrEmpty(stanzaId)) {
6433            return;
6434        }
6435        final Conversation conversation;
6436        final var conversational = message.getConversation();
6437        if (conversational instanceof Conversation c) {
6438            conversation = c;
6439        } else {
6440            return;
6441        }
6442        final var account = conversation.getAccount();
6443        final var connection = account.getXmppConnection();
6444        if (connection == null || !connection.getFeatures().mds()) {
6445            return;
6446        }
6447        final Jid itemId;
6448        if (message.isPrivateMessage()) {
6449            itemId = message.getCounterpart();
6450        } else {
6451            itemId = conversation.getJid().asBareJid();
6452        }
6453        Log.d(Config.LOGTAG, "publishing mds for " + itemId + "/" + stanzaId);
6454        publishMds(account, itemId, stanzaId, conversation);
6455    }
6456
6457    private void publishMds(
6458            final Account account,
6459            final Jid itemId,
6460            final String stanzaId,
6461            final Conversation conversation) {
6462        final var item = mIqGenerator.mdsDisplayed(stanzaId, conversation);
6463        pushNodeAndEnforcePublishOptions(
6464                account,
6465                Namespace.MDS_DISPLAYED,
6466                item,
6467                itemId.toString(),
6468                PublishOptions.persistentWhitelistAccessMaxItems());
6469    }
6470
6471    public boolean sendReactions(final Message message, final Collection<String> reactions) {
6472        if (message.isPrivateMessage()) throw new IllegalArgumentException("Reactions to PM not implemented");
6473        if (message.getConversation() instanceof Conversation conversation) {
6474            final var isPrivateMessage = message.isPrivateMessage();
6475            final Jid reactTo;
6476            final boolean typeGroupChat;
6477            final String reactToId;
6478            final Collection<Reaction> combinedReactions;
6479            final var newReactions = new HashSet<>(reactions);
6480            newReactions.removeAll(message.getAggregatedReactions().ourReactions);
6481            if (conversation.getMode() == Conversational.MODE_MULTI && !isPrivateMessage) {
6482                final var mucOptions = conversation.getMucOptions();
6483                if (!mucOptions.participating()) {
6484                    Log.e(Config.LOGTAG, "not participating in MUC");
6485                    return false;
6486                }
6487                final var self = mucOptions.getSelf();
6488                final String occupantId = self.getOccupantId();
6489                if (Strings.isNullOrEmpty(occupantId)) {
6490                    Log.e(Config.LOGTAG, "occupant id not found for reaction in MUC");
6491                    return false;
6492                }
6493                final var existingRaw =
6494                        ImmutableSet.copyOf(
6495                                Collections2.transform(message.getReactions(), r -> r.reaction));
6496                final var reactionsAsExistingVariants =
6497                        ImmutableSet.copyOf(
6498                                Collections2.transform(
6499                                        reactions, r -> Emoticons.existingVariant(r, existingRaw)));
6500                if (!reactions.equals(reactionsAsExistingVariants)) {
6501                    Log.d(Config.LOGTAG, "modified reactions to existing variants");
6502                }
6503                reactToId = message.getServerMsgId();
6504                reactTo = conversation.getJid().asBareJid();
6505                typeGroupChat = true;
6506                combinedReactions =
6507                        Reaction.withMine(
6508                                message.getReactions(),
6509                                reactionsAsExistingVariants,
6510                                false,
6511                                self.getFullJid(),
6512                                conversation.getAccount().getJid(),
6513                                occupantId,
6514                                null);
6515            } else {
6516                if (message.isCarbon() || message.getStatus() == Message.STATUS_RECEIVED) {
6517                    reactToId = message.getRemoteMsgId();
6518                } else {
6519                    reactToId = message.getUuid();
6520                }
6521                typeGroupChat = false;
6522                if (isPrivateMessage) {
6523                    reactTo = message.getCounterpart();
6524                } else {
6525                    reactTo = conversation.getJid().asBareJid();
6526                }
6527                combinedReactions =
6528                        Reaction.withFrom(
6529                                message.getReactions(),
6530                                reactions,
6531                                false,
6532                                conversation.getAccount().getJid(),
6533                                null);
6534            }
6535            if (reactTo == null || Strings.isNullOrEmpty(reactToId)) {
6536                Log.e(Config.LOGTAG, "could not find id to react to");
6537                return false;
6538            }
6539
6540            final var packet =
6541                    mMessageGenerator.reaction(reactTo, typeGroupChat, message, reactToId, reactions);
6542
6543            final var quote = QuoteHelper.quote(MessageUtils.prepareQuote(message)) + "\n";
6544            final var body  = quote + String.join(" ", newReactions);
6545            if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && newReactions.size() > 0) {
6546                FILE_ATTACHMENT_EXECUTOR.execute(() -> {
6547                    XmppAxolotlMessage axolotlMessage = conversation.getAccount().getAxolotlService().encrypt(body, conversation);
6548                    packet.setAxolotlMessage(axolotlMessage.toElement());
6549                    packet.addChild("encryption", "urn:xmpp:eme:0")
6550                        .setAttribute("name", "OMEMO")
6551                        .setAttribute("namespace", AxolotlService.PEP_PREFIX);
6552                    sendMessagePacket(conversation.getAccount(), packet);
6553                    message.setReactions(combinedReactions);
6554                    updateMessage(message, false);
6555                });
6556            } else if (conversation.getNextEncryption() == Message.ENCRYPTION_NONE || newReactions.size() < 1) {
6557                if (newReactions.size() > 0) {
6558                    packet.setBody(body);
6559
6560                    packet.addChild("reply", "urn:xmpp:reply:0")
6561                        .setAttribute("to", message.getCounterpart())
6562                        .setAttribute("id", reactToId);
6563                    final var replyFallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reply:0");
6564                    replyFallback.addChild("body", "urn:xmpp:fallback:0")
6565                        .setAttribute("start", "0")
6566                        .setAttribute("end", "" + quote.codePointCount(0, quote.length()));
6567
6568                    final var fallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reactions:0");
6569                    fallback.addChild("body", "urn:xmpp:fallback:0");
6570                }
6571
6572                sendMessagePacket(conversation.getAccount(), packet);
6573                message.setReactions(combinedReactions);
6574                updateMessage(message, false);
6575            }
6576
6577            return true;
6578        } else {
6579            return false;
6580        }
6581    }
6582
6583    public MemorizingTrustManager getMemorizingTrustManager() {
6584        return this.mMemorizingTrustManager;
6585    }
6586
6587    public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
6588        this.mMemorizingTrustManager = trustManager;
6589    }
6590
6591    public void updateMemorizingTrustManager() {
6592        final MemorizingTrustManager trustManager;
6593        if (appSettings.isTrustSystemCAStore()) {
6594            trustManager = new MemorizingTrustManager(getApplicationContext());
6595        } else {
6596            trustManager = new MemorizingTrustManager(getApplicationContext(), null);
6597        }
6598        setMemorizingTrustManager(trustManager);
6599    }
6600
6601    public LruCache<String, Drawable> getDrawableCache() {
6602        return this.mDrawableCache;
6603    }
6604
6605    public Collection<String> getKnownHosts() {
6606        final Set<String> hosts = new HashSet<>();
6607        for (final Account account : getAccounts()) {
6608            hosts.add(account.getServer());
6609            for (final Contact contact : account.getRoster().getContacts()) {
6610                if (contact.showInRoster()) {
6611                    final String server = contact.getServer();
6612                    if (server != null) {
6613                        hosts.add(server);
6614                    }
6615                }
6616            }
6617        }
6618        if (Config.QUICKSY_DOMAIN != null) {
6619            hosts.remove(
6620                    Config.QUICKSY_DOMAIN
6621                            .toString()); // we only want to show this when we type a e164
6622            // number
6623        }
6624        if (Config.MAGIC_CREATE_DOMAIN != null) {
6625            hosts.add(Config.MAGIC_CREATE_DOMAIN);
6626        }
6627        hosts.add("chat.above.im");
6628        return hosts;
6629    }
6630
6631    public Collection<String> getKnownConferenceHosts() {
6632        final Set<String> mucServers = new HashSet<>();
6633        for (final Account account : accounts) {
6634            if (account.getXmppConnection() != null) {
6635                mucServers.addAll(account.getXmppConnection().getMucServers());
6636                for (final Bookmark bookmark : account.getBookmarks()) {
6637                    final Jid jid = bookmark.getJid();
6638                    final String s = jid == null ? null : jid.getDomain().toString();
6639                    if (s != null) {
6640                        mucServers.add(s);
6641                    }
6642                }
6643            }
6644        }
6645        return mucServers;
6646    }
6647
6648    public void sendMessagePacket(
6649            final Account account,
6650            final im.conversations.android.xmpp.model.stanza.Message packet) {
6651        final XmppConnection connection = account.getXmppConnection();
6652        if (connection != null) {
6653            connection.sendMessagePacket(packet);
6654        }
6655    }
6656
6657    public void sendPresencePacket(
6658            final Account account,
6659            final im.conversations.android.xmpp.model.stanza.Presence packet) {
6660        final XmppConnection connection = account.getXmppConnection();
6661        if (connection != null) {
6662            connection.sendPresencePacket(packet);
6663        }
6664    }
6665
6666    public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
6667        final XmppConnection connection = account.getXmppConnection();
6668        if (connection == null) {
6669            return;
6670        }
6671        connection.sendCreateAccountWithCaptchaPacket(id, data);
6672    }
6673
6674    public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback) {
6675        sendIqPacket(account, packet, callback, null);
6676    }
6677
6678    public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback, Long timeout) {
6679        final XmppConnection connection = account.getXmppConnection();
6680        if (connection != null) {
6681            connection.sendIqPacket(packet, callback, timeout);
6682        } else if (callback != null) {
6683            callback.accept(Iq.TIMEOUT);
6684        }
6685    }
6686
6687    public void sendPresence(final Account account) {
6688        sendPresence(account, checkListeners() && broadcastLastActivity());
6689    }
6690
6691    private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
6692        final Presence.Status status;
6693        if (manuallyChangePresence()) {
6694            status = account.getPresenceStatus();
6695        } else {
6696            status = getTargetPresence();
6697        }
6698        final var packet = mPresenceGenerator.selfPresence(account, status);
6699        if (mLastActivity > 0 && includeIdleTimestamp) {
6700            long since =
6701                    Math.min(mLastActivity, System.currentTimeMillis()); // don't send future dates
6702            packet.addChild("idle", Namespace.IDLE)
6703                    .setAttribute("since", AbstractGenerator.getTimestamp(since));
6704        }
6705        sendPresencePacket(account, packet);
6706    }
6707
6708    private void deactivateGracePeriod() {
6709        for (Account account : getAccounts()) {
6710            account.deactivateGracePeriod();
6711        }
6712    }
6713
6714    public void refreshAllPresences() {
6715        boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
6716        for (Account account : getAccounts()) {
6717            if (account.isConnectionEnabled()) {
6718                sendPresence(account, includeIdleTimestamp);
6719            }
6720        }
6721    }
6722
6723    private void refreshAllFcmTokens() {
6724        for (Account account : getAccounts()) {
6725            if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
6726                mPushManagementService.registerPushTokenOnServer(account);
6727            }
6728        }
6729    }
6730
6731    private void sendOfflinePresence(final Account account) {
6732        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence");
6733        sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
6734    }
6735
6736    public MessageGenerator getMessageGenerator() {
6737        return this.mMessageGenerator;
6738    }
6739
6740    public PresenceGenerator getPresenceGenerator() {
6741        return this.mPresenceGenerator;
6742    }
6743
6744    public IqGenerator getIqGenerator() {
6745        return this.mIqGenerator;
6746    }
6747
6748    public JingleConnectionManager getJingleConnectionManager() {
6749        return this.mJingleConnectionManager;
6750    }
6751
6752    private boolean hasJingleRtpConnection(final Account account) {
6753        return this.mJingleConnectionManager.hasJingleRtpConnection(account);
6754    }
6755
6756    public MessageArchiveService getMessageArchiveService() {
6757        return this.mMessageArchiveService;
6758    }
6759
6760    public QuickConversationsService getQuickConversationsService() {
6761        return this.mQuickConversationsService;
6762    }
6763
6764    public List<Contact> findContacts(Jid jid, String accountJid) {
6765        ArrayList<Contact> contacts = new ArrayList<>();
6766        for (Account account : getAccounts()) {
6767            if ((account.isEnabled() || accountJid != null)
6768                    && (accountJid == null
6769                            || accountJid.equals(account.getJid().asBareJid().toString()))) {
6770                Contact contact = account.getRoster().getContactFromContactList(jid);
6771                if (contact != null) {
6772                    contacts.add(contact);
6773                }
6774            }
6775        }
6776        return contacts;
6777    }
6778
6779    public Conversation findFirstMuc(Jid jid) {
6780        return findFirstMuc(jid, null);
6781    }
6782
6783    public Conversation findFirstMuc(Jid jid, String accountJid) {
6784        for (Conversation conversation : getConversations()) {
6785            if ((conversation.getAccount().isEnabled() || accountJid != null)
6786                    && (accountJid == null || accountJid.equals(conversation.getAccount().getJid().asBareJid().toString()))
6787                    && conversation.getJid().asBareJid().equals(jid.asBareJid()) && conversation.getMode() == Conversation.MODE_MULTI) {
6788                return conversation;
6789            }
6790        }
6791        return null;
6792    }
6793
6794    public NotificationService getNotificationService() {
6795        return this.mNotificationService;
6796    }
6797
6798    public HttpConnectionManager getHttpConnectionManager() {
6799        return this.mHttpConnectionManager;
6800    }
6801
6802    public void resendFailedMessages(final Message message, final boolean forceP2P) {
6803        message.setTime(System.currentTimeMillis());
6804        markMessage(message, Message.STATUS_WAITING);
6805        this.sendMessage(message, true, false, false, forceP2P, null);
6806        if (message.getConversation() instanceof Conversation c) {
6807            c.sort();
6808        }
6809        updateConversationUi();
6810    }
6811
6812    public void clearConversationHistory(final Conversation conversation) {
6813        final long clearDate;
6814        final String reference;
6815        if (conversation.countMessages() > 0) {
6816            Message latestMessage = conversation.getLatestMessage();
6817            clearDate = latestMessage.getTimeSent() + 1000;
6818            reference = latestMessage.getServerMsgId();
6819        } else {
6820            clearDate = System.currentTimeMillis();
6821            reference = null;
6822        }
6823        conversation.clearMessages();
6824        conversation.setHasMessagesLeftOnServer(false); // avoid messages getting loaded through mam
6825        conversation.setLastClearHistory(clearDate, reference);
6826        Runnable runnable =
6827                () -> {
6828                    databaseBackend.deleteMessagesInConversation(conversation);
6829                    databaseBackend.updateConversation(conversation);
6830                };
6831        mDatabaseWriterExecutor.execute(runnable);
6832    }
6833
6834    public boolean sendBlockRequest(
6835            final Blockable blockable, final boolean reportSpam, final String serverMsgId) {
6836        if (blockable != null && blockable.getBlockedJid() != null) {
6837            final var account = blockable.getAccount();
6838            final Jid jid = blockable.getBlockedJid();
6839            this.sendIqPacket(
6840                    account,
6841                    getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId),
6842                    (response) -> {
6843                        if (response.getType() == Iq.Type.RESULT) {
6844                            account.getBlocklist().add(jid);
6845                            updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
6846                        }
6847                    });
6848            if (blockable.getBlockedJid().isFullJid()) {
6849                return false;
6850            } else if (removeBlockedConversations(blockable.getAccount(), jid)) {
6851                updateConversationUi();
6852                return true;
6853            } else {
6854                return false;
6855            }
6856        } else {
6857            return false;
6858        }
6859    }
6860
6861    public boolean removeBlockedConversations(final Account account, final Jid blockedJid) {
6862        boolean removed = false;
6863        synchronized (this.conversations) {
6864            boolean domainJid = blockedJid.getLocal() == null;
6865            for (Conversation conversation : this.conversations) {
6866                boolean jidMatches =
6867                        (domainJid
6868                                        && blockedJid
6869                                                .getDomain()
6870                                                .equals(conversation.getJid().getDomain()))
6871                                || blockedJid.equals(conversation.getJid().asBareJid());
6872                if (conversation.getAccount() == account
6873                        && conversation.getMode() == Conversation.MODE_SINGLE
6874                        && jidMatches) {
6875                    this.conversations.remove(conversation);
6876                    markRead(conversation);
6877                    conversation.setStatus(Conversation.STATUS_ARCHIVED);
6878                    Log.d(
6879                            Config.LOGTAG,
6880                            account.getJid().asBareJid()
6881                                    + ": archiving conversation "
6882                                    + conversation.getJid().asBareJid()
6883                                    + " because jid was blocked");
6884                    updateConversation(conversation);
6885                    removed = true;
6886                }
6887            }
6888        }
6889        return removed;
6890    }
6891
6892    public void sendUnblockRequest(final Blockable blockable) {
6893        if (blockable != null && blockable.getJid() != null) {
6894            final var account = blockable.getAccount();
6895            final Jid jid = blockable.getBlockedJid();
6896            this.sendIqPacket(
6897                    account,
6898                    getIqGenerator().generateSetUnblockRequest(jid),
6899                    response -> {
6900                        if (response.getType() == Iq.Type.RESULT) {
6901                            account.getBlocklist().remove(jid);
6902                            updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
6903                        }
6904                    });
6905        }
6906    }
6907
6908    public void publishDisplayName(final Account account) {
6909        String displayName = account.getDisplayName();
6910        final Iq request;
6911        if (TextUtils.isEmpty(displayName)) {
6912            request = mIqGenerator.deleteNode(Namespace.NICK);
6913        } else {
6914            request = mIqGenerator.publishNick(displayName);
6915        }
6916        mAvatarService.clear(account);
6917        sendIqPacket(
6918                account,
6919                request,
6920                (packet) -> {
6921                    if (packet.getType() == Iq.Type.ERROR) {
6922                        Log.d(
6923                                Config.LOGTAG,
6924                                account.getJid().asBareJid()
6925                                        + ": unable to modify nick name "
6926                                        + packet);
6927                    }
6928                });
6929    }
6930
6931    public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String, String> key) {
6932        ServiceDiscoveryResult result = discoCache.get(key);
6933        if (result != null) {
6934            return result;
6935        } else {
6936            if (key.first == null || key.second == null) return null;
6937            result = databaseBackend.findDiscoveryResult(key.first, key.second);
6938            if (result != null) {
6939                discoCache.put(key, result);
6940            }
6941            return result;
6942        }
6943    }
6944
6945    public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) {
6946        final var request = new Iq(input == null ? Iq.Type.GET : Iq.Type.SET);
6947        request.setTo(jid);
6948        Element query = request.query("jabber:iq:gateway");
6949        if (input != null) {
6950            Element prompt = query.addChild("prompt");
6951            prompt.setContent(input);
6952        }
6953        sendIqPacket(account, request, packet -> {
6954            if (packet.getType() == Iq.Type.RESULT) {
6955                callback.onGatewayResult(packet.query().findChildContent(input == null ? "prompt" : "jid"), null);
6956            } else {
6957                Element error = packet.findChild("error");
6958                callback.onGatewayResult(null, error == null ? null : error.findChildContent("text"));
6959            }
6960        });
6961    }
6962
6963    public void fetchCaps(Account account, final Jid jid, final Presence presence) {
6964        fetchCaps(account, jid, presence, null);
6965    }
6966
6967    public void fetchCaps(Account account, final Jid jid, final Presence presence, Runnable cb) {
6968        final Pair<String, String> key = presence == null ? null : new Pair<>(presence.getHash(), presence.getVer());
6969        final ServiceDiscoveryResult disco = key == null ? null : getCachedServiceDiscoveryResult(key);
6970
6971        if (disco != null) {
6972            presence.setServiceDiscoveryResult(disco);
6973            final Contact contact = account.getRoster().getContact(jid);
6974            if (contact.refreshRtpCapability()) {
6975                syncRoster(account);
6976            }
6977            contact.refreshCaps();
6978            if (disco.hasIdentity("gateway", "pstn")) {
6979                contact.registerAsPhoneAccount(this);
6980                mQuickConversationsService.considerSyncBackground(false);
6981            }
6982            updateConversationUi(true);
6983        } else {
6984            final Iq request = new Iq(Iq.Type.GET);
6985            request.setTo(jid);
6986            final String node = presence == null ? null : presence.getNode();
6987            final String ver = presence == null ? null : presence.getVer();
6988            final Element query = request.query(Namespace.DISCO_INFO);
6989            if (node != null && ver != null) {
6990                query.setAttribute("node", node + "#" + ver);
6991            }
6992
6993            Log.d(
6994                    Config.LOGTAG,
6995                    account.getJid().asBareJid()
6996                            + ": making disco request for "
6997                            + (key == null ? null : key.second)
6998                            + " to "
6999                            + jid);
7000            sendIqPacket(
7001                    account,
7002                    request,
7003                    (response) -> {
7004                        if (response.getType() == Iq.Type.RESULT) {
7005                            final ServiceDiscoveryResult discoveryResult =
7006                                    new ServiceDiscoveryResult(response);
7007                            if (presence == null || presence.getVer() == null || presence.getVer().equals(discoveryResult.getVer())) {
7008                                databaseBackend.insertDiscoveryResult(discoveryResult);
7009                                injectServiceDiscoveryResult(
7010                                        account.getRoster(),
7011                                        presence == null ? null : presence.getHash(),
7012                                        presence == null ? null : presence.getVer(),
7013                                        jid.getResource(),
7014                                        discoveryResult);
7015                                if (discoveryResult.hasIdentity("gateway", "pstn")) {
7016                                    final Contact contact = account.getRoster().getContact(jid);
7017                                    contact.registerAsPhoneAccount(this);
7018                                    mQuickConversationsService.considerSyncBackground(false);
7019                                }
7020                                updateConversationUi(true);
7021                                if (cb != null) cb.run();
7022                            } else {
7023                                Log.d(
7024                                        Config.LOGTAG,
7025                                        account.getJid().asBareJid()
7026                                                + ": mismatch in caps for contact "
7027                                                + jid
7028                                                + " "
7029                                                + presence.getVer()
7030                                                + " vs "
7031                                                + discoveryResult.getVer());
7032                            }
7033                        } else {
7034                            Log.d(
7035                                    Config.LOGTAG,
7036                                    account.getJid().asBareJid()
7037                                            + ": unable to fetch caps from "
7038                                            + jid);
7039                        }
7040                    });
7041        }
7042    }
7043
7044    public void fetchCommands(Account account, final Jid jid, Consumer<Iq> callback) {
7045        final var request = mIqGenerator.queryDiscoItems(jid, "http://jabber.org/protocol/commands");
7046        sendIqPacket(account, request, callback);
7047    }
7048
7049    private void injectServiceDiscoveryResult(
7050            Roster roster, String hash, String ver, String resource, ServiceDiscoveryResult disco) {
7051        boolean rosterNeedsSync = false;
7052        for (final Contact contact : roster.getContacts()) {
7053            boolean serviceDiscoverySet = false;
7054            Presence onePresence = contact.getPresences().get(resource == null ? "" : resource);
7055            if (onePresence != null) {
7056                onePresence.setServiceDiscoveryResult(disco);
7057                serviceDiscoverySet = true;
7058            } else if (resource == null && hash == null && ver == null) {
7059                Presence p = new Presence(Presence.Status.OFFLINE, null, null, null, "");
7060                p.setServiceDiscoveryResult(disco);
7061                contact.updatePresence("", p);
7062                serviceDiscoverySet = true;
7063            }
7064            if (hash != null && ver != null) {
7065                for (final Presence presence : contact.getPresences().getPresences()) {
7066                    if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
7067                        presence.setServiceDiscoveryResult(disco);
7068                        serviceDiscoverySet = true;
7069                    }
7070                }
7071            }
7072            if (serviceDiscoverySet) {
7073                rosterNeedsSync |= contact.refreshRtpCapability();
7074                contact.refreshCaps();
7075            }
7076        }
7077        if (rosterNeedsSync) {
7078            syncRoster(roster.getAccount());
7079        }
7080    }
7081
7082    public void fetchMamPreferences(final Account account, final OnMamPreferencesFetched callback) {
7083        final MessageArchiveService.Version version = MessageArchiveService.Version.get(account);
7084        final Iq request = new Iq(Iq.Type.GET);
7085        request.addChild("prefs", version.namespace);
7086        sendIqPacket(
7087                account,
7088                request,
7089                (packet) -> {
7090                    final Element prefs = packet.findChild("prefs", version.namespace);
7091                    if (packet.getType() == Iq.Type.RESULT && prefs != null) {
7092                        callback.onPreferencesFetched(prefs);
7093                    } else {
7094                        callback.onPreferencesFetchFailed();
7095                    }
7096                });
7097    }
7098
7099    public PushManagementService getPushManagementService() {
7100        return mPushManagementService;
7101    }
7102
7103    public void changeStatus(Account account, PresenceTemplate template, String signature) {
7104        if (!template.getStatusMessage().isEmpty()) {
7105            databaseBackend.insertPresenceTemplate(template);
7106        }
7107        account.setPgpSignature(signature);
7108        account.setPresenceStatus(template.getStatus());
7109        account.setPresenceStatusMessage(template.getStatusMessage());
7110        databaseBackend.updateAccount(account);
7111        sendPresence(account);
7112    }
7113
7114    public List<PresenceTemplate> getPresenceTemplates(Account account) {
7115        List<PresenceTemplate> templates = databaseBackend.getPresenceTemplates();
7116        for (PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) {
7117            if (!templates.contains(template)) {
7118                templates.add(0, template);
7119            }
7120        }
7121        return templates;
7122    }
7123
7124    public void saveConversationAsBookmark(final Conversation conversation, final String name) {
7125        final Account account = conversation.getAccount();
7126        final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid());
7127        String nick = conversation.getMucOptions().getActualNick();
7128        if (nick == null) nick = conversation.getJid().getResource();
7129        if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
7130            bookmark.setNick(nick);
7131        }
7132        if (!TextUtils.isEmpty(name)) {
7133            bookmark.setBookmarkName(name);
7134        }
7135        bookmark.setAutojoin(true);
7136        createBookmark(account, bookmark);
7137        bookmark.setConversation(conversation);
7138    }
7139
7140    public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
7141        boolean performedVerification = false;
7142        final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
7143        for (XmppUri.Fingerprint fp : fingerprints) {
7144            if (fp.type == XmppUri.FingerprintType.OMEMO) {
7145                String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
7146                FingerprintStatus fingerprintStatus =
7147                        axolotlService.getFingerprintTrust(fingerprint);
7148                if (fingerprintStatus != null) {
7149                    if (!fingerprintStatus.isVerified()) {
7150                        performedVerification = true;
7151                        axolotlService.setFingerprintTrust(
7152                                fingerprint, fingerprintStatus.toVerified());
7153                    }
7154                } else {
7155                    axolotlService.preVerifyFingerprint(contact, fingerprint);
7156                }
7157            }
7158        }
7159        return performedVerification;
7160    }
7161
7162    public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
7163        final AxolotlService axolotlService = account.getAxolotlService();
7164        boolean verifiedSomething = false;
7165        for (XmppUri.Fingerprint fp : fingerprints) {
7166            if (fp.type == XmppUri.FingerprintType.OMEMO) {
7167                String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
7168                Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint);
7169                FingerprintStatus fingerprintStatus =
7170                        axolotlService.getFingerprintTrust(fingerprint);
7171                if (fingerprintStatus != null) {
7172                    if (!fingerprintStatus.isVerified()) {
7173                        axolotlService.setFingerprintTrust(
7174                                fingerprint, fingerprintStatus.toVerified());
7175                        verifiedSomething = true;
7176                    }
7177                } else {
7178                    axolotlService.preVerifyFingerprint(account, fingerprint);
7179                    verifiedSomething = true;
7180                }
7181            }
7182        }
7183        return verifiedSomething;
7184    }
7185
7186    public ShortcutService getShortcutService() {
7187        return mShortcutService;
7188    }
7189
7190    public void pushMamPreferences(Account account, Element prefs) {
7191        final Iq set = new Iq(Iq.Type.SET);
7192        set.addChild(prefs);
7193        account.setMamPrefs(prefs);
7194        sendIqPacket(account, set, null);
7195    }
7196
7197    public void evictPreview(File f) {
7198        if (f == null) return;
7199
7200        if (mDrawableCache.remove(f.getAbsolutePath()) != null) {
7201            Log.d(Config.LOGTAG, "deleted cached preview");
7202        }
7203    }
7204
7205    public void evictPreview(String uuid) {
7206        if (mDrawableCache.remove(uuid) != null) {
7207            Log.d(Config.LOGTAG, "deleted cached preview");
7208        }
7209    }
7210
7211    public interface OnMamPreferencesFetched {
7212        void onPreferencesFetched(Element prefs);
7213
7214        void onPreferencesFetchFailed();
7215    }
7216
7217    public interface OnAccountCreated {
7218        void onAccountCreated(Account account);
7219
7220        void informUser(int r);
7221    }
7222
7223    public interface OnMoreMessagesLoaded {
7224        void onMoreMessagesLoaded(int count, Conversation conversation);
7225
7226        void informUser(int r);
7227    }
7228
7229    public interface OnAccountPasswordChanged {
7230        void onPasswordChangeSucceeded();
7231
7232        void onPasswordChangeFailed();
7233    }
7234
7235    public interface OnRoomDestroy {
7236        void onRoomDestroySucceeded();
7237
7238        void onRoomDestroyFailed();
7239    }
7240
7241    public interface OnAffiliationChanged {
7242        void onAffiliationChangedSuccessful(Jid jid);
7243
7244        void onAffiliationChangeFailed(Jid jid, int resId);
7245    }
7246
7247    public interface OnConversationUpdate {
7248        default void onConversationUpdate() { onConversationUpdate(false); }
7249        default void onConversationUpdate(boolean newCaps) { onConversationUpdate(); }
7250    }
7251
7252    public interface OnJingleRtpConnectionUpdate {
7253        void onJingleRtpConnectionUpdate(
7254                final Account account,
7255                final Jid with,
7256                final String sessionId,
7257                final RtpEndUserState state);
7258
7259        void onAudioDeviceChanged(
7260                CallIntegration.AudioDevice selectedAudioDevice,
7261                Set<CallIntegration.AudioDevice> availableAudioDevices);
7262    }
7263
7264    public interface OnAccountUpdate {
7265        void onAccountUpdate();
7266    }
7267
7268    public interface OnCaptchaRequested {
7269        void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha);
7270    }
7271
7272    public interface OnRosterUpdate {
7273        void onRosterUpdate(final UpdateRosterReason reason, final Contact contact);
7274    }
7275
7276    public interface OnMucRosterUpdate {
7277        void onMucRosterUpdate();
7278    }
7279
7280    public interface OnConferenceConfigurationFetched {
7281        void onConferenceConfigurationFetched(Conversation conversation);
7282
7283        void onFetchFailed(Conversation conversation, String errorCondition);
7284    }
7285
7286    public interface OnConferenceJoined {
7287        void onConferenceJoined(Conversation conversation);
7288    }
7289
7290    public interface OnConfigurationPushed {
7291        void onPushSucceeded();
7292
7293        void onPushFailed();
7294    }
7295
7296    public interface OnShowErrorToast {
7297        void onShowErrorToast(int resId);
7298    }
7299
7300    public class XmppConnectionBinder extends Binder {
7301        public XmppConnectionService getService() {
7302            return XmppConnectionService.this;
7303        }
7304    }
7305
7306    private class InternalEventReceiver extends BroadcastReceiver {
7307
7308        @Override
7309        public void onReceive(final Context context, final Intent intent) {
7310            onStartCommand(intent, 0, 0);
7311        }
7312    }
7313
7314    private class RestrictedEventReceiver extends BroadcastReceiver {
7315
7316        private final Collection<String> allowedActions;
7317
7318        private RestrictedEventReceiver(final Collection<String> allowedActions) {
7319            this.allowedActions = allowedActions;
7320        }
7321
7322        @Override
7323        public void onReceive(final Context context, final Intent intent) {
7324            final String action = intent == null ? null : intent.getAction();
7325            if (allowedActions.contains(action)) {
7326                onStartCommand(intent, 0, 0);
7327            } else {
7328                Log.e(Config.LOGTAG, "restricting broadcast of event " + action);
7329            }
7330        }
7331    }
7332
7333    public static class OngoingCall {
7334        public final AbstractJingleConnection.Id id;
7335        public final Set<Media> media;
7336        public final boolean reconnecting;
7337
7338        public OngoingCall(
7339                AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
7340            this.id = id;
7341            this.media = media;
7342            this.reconnecting = reconnecting;
7343        }
7344
7345        @Override
7346        public boolean equals(Object o) {
7347            if (this == o) return true;
7348            if (o == null || getClass() != o.getClass()) return false;
7349            OngoingCall that = (OngoingCall) o;
7350            return reconnecting == that.reconnecting
7351                    && Objects.equal(id, that.id)
7352                    && Objects.equal(media, that.media);
7353        }
7354
7355        @Override
7356        public int hashCode() {
7357            return Objects.hashCode(id, media, reconnecting);
7358        }
7359    }
7360
7361    public static void toggleForegroundService(final XmppConnectionService service) {
7362        if (service == null) {
7363            return;
7364        }
7365        service.toggleForegroundService();
7366    }
7367
7368    public static void toggleForegroundService(final ConversationsActivity activity) {
7369        if (activity == null) {
7370            return;
7371        }
7372        toggleForegroundService(activity.xmppConnectionService);
7373    }
7374
7375    public static class BlockedMediaException extends Exception { }
7376
7377    public static enum UpdateRosterReason {
7378        INIT,
7379        AVATAR,
7380        PUSH,
7381        PRESENCE
7382    }
7383}