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.runsTwentySix()) {
1690            mNotificationService.initializeChannels();
1691        }
1692        mChannelDiscoveryService.initializeMuclumbusService();
1693        mForceDuringOnCreate.set(Compatibility.runsAndTargetsTwentySix(this));
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                || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) {
1961            if (Compatibility.runsTwentySix()) {
1962                mNotificationService.initializeChannels();
1963            }
1964            final Notification notification;
1965            if (ongoing != null && !diallerIntegrationActive.get()) {
1966                notification = this.mNotificationService.getOngoingCallNotification(ongoing);
1967                id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
1968                startForegroundOrCatch(id, notification, true);
1969            } else if (ongoingVideoTranscoding) {
1970                notification = this.mNotificationService.getIndeterminateVideoTranscoding();
1971                id = NotificationService.ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID;
1972                startForegroundOrCatch(id, notification, false);
1973            } else {
1974                notification = this.mNotificationService.createForegroundNotification();
1975                id = NotificationService.FOREGROUND_NOTIFICATION_ID;
1976                startForegroundOrCatch(id, notification, needMic || ongoing != null || diallerIntegrationActive.get());
1977            }
1978            mNotificationService.notify(id, notification);
1979            status = true;
1980        } else {
1981            id = 0;
1982            stopForeground(true);
1983            status = false;
1984        }
1985
1986        for (final int toBeRemoved :
1987                Collections2.filter(
1988                        Arrays.asList(
1989                                NotificationService.FOREGROUND_NOTIFICATION_ID,
1990                                NotificationService.ONGOING_CALL_NOTIFICATION_ID,
1991                                NotificationService.ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID),
1992                        i -> i != id)) {
1993            mNotificationService.cancel(toBeRemoved);
1994        }
1995        Log.d(
1996                Config.LOGTAG,
1997                "ForegroundService: " + (status ? "on" : "off") + ", notification: " + id);
1998    }
1999
2000    private void startForegroundOrCatch(
2001            final int id, final Notification notification, final boolean requireMicrophone) {
2002        try {
2003            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
2004                final int foregroundServiceType;
2005                if (requireMicrophone
2006                        && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
2007                                == PackageManager.PERMISSION_GRANTED) {
2008                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
2009                    Log.d(Config.LOGTAG, "defaulting to microphone foreground service type");
2010                } else if (getSystemService(PowerManager.class)
2011                        .isIgnoringBatteryOptimizations(getPackageName())) {
2012                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED;
2013                } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
2014                        == PackageManager.PERMISSION_GRANTED) {
2015                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
2016                } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
2017                        == PackageManager.PERMISSION_GRANTED) {
2018                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
2019                } else {
2020                    foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
2021                    Log.w(Config.LOGTAG, "falling back to special use foreground service type");
2022                }
2023
2024                startForeground(id, notification, foregroundServiceType);
2025            } else {
2026                startForeground(id, notification);
2027            }
2028        } catch (final IllegalStateException | SecurityException e) {
2029            Log.e(Config.LOGTAG, "Could not start foreground service", e);
2030        }
2031    }
2032
2033    public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() {
2034        return !mOngoingVideoTranscoding.get()
2035                && ongoingCall.get() == null
2036                && Compatibility.keepForegroundService(this)
2037                && hasEnabledAccounts();
2038    }
2039
2040    @Override
2041    public void onTaskRemoved(final Intent rootIntent) {
2042        super.onTaskRemoved(rootIntent);
2043        if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts())
2044                || mOngoingVideoTranscoding.get()
2045                || ongoingCall.get() != null) {
2046            Log.d(Config.LOGTAG, "ignoring onTaskRemoved because foreground service is activated");
2047        } else {
2048            this.logoutAndSave(false);
2049        }
2050    }
2051
2052    private void logoutAndSave(boolean stop) {
2053        int activeAccounts = 0;
2054        for (final Account account : accounts) {
2055            if (account.isConnectionEnabled()) {
2056                databaseBackend.writeRoster(account.getRoster());
2057                activeAccounts++;
2058            }
2059            if (account.getXmppConnection() != null) {
2060                new Thread(() -> disconnect(account, false)).start();
2061            }
2062        }
2063        if (stop || activeAccounts == 0) {
2064            Log.d(Config.LOGTAG, "good bye");
2065            stopSelf();
2066        }
2067    }
2068
2069    private void schedulePostConnectivityChange() {
2070        final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
2071        if (alarmManager == null) {
2072            return;
2073        }
2074        final long triggerAtMillis =
2075                SystemClock.elapsedRealtime()
2076                        + (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL * 1000);
2077        final Intent intent = new Intent(this, SystemEventReceiver.class);
2078        intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE);
2079        try {
2080            final PendingIntent pendingIntent =
2081                    PendingIntent.getBroadcast(
2082                            this,
2083                            1,
2084                            intent,
2085                            s()
2086                                    ? PendingIntent.FLAG_IMMUTABLE
2087                                            | PendingIntent.FLAG_UPDATE_CURRENT
2088                                    : PendingIntent.FLAG_UPDATE_CURRENT);
2089            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
2090                alarmManager.setAndAllowWhileIdle(
2091                        AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
2092            } else {
2093                alarmManager.set(
2094                        AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
2095            }
2096        } catch (RuntimeException e) {
2097            Log.e(Config.LOGTAG, "unable to schedule alarm for post connectivity change", e);
2098        }
2099    }
2100
2101    public void scheduleWakeUpCall(final int seconds, final int requestCode) {
2102        scheduleWakeUpCall((seconds < 0 ? 1 : seconds + 1) * 1000L, requestCode);
2103    }
2104
2105    public void scheduleWakeUpCall(final long milliSeconds, final int requestCode) {
2106        final var timeToWake = SystemClock.elapsedRealtime() + milliSeconds;
2107        final var alarmManager = getSystemService(AlarmManager.class);
2108        final Intent intent = new Intent(this, SystemEventReceiver.class);
2109        intent.setAction(ACTION_PING);
2110        try {
2111            final PendingIntent pendingIntent =
2112                    PendingIntent.getBroadcast(
2113                            this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE);
2114            alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
2115        } catch (final RuntimeException e) {
2116            Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e);
2117        }
2118    }
2119
2120    private void scheduleNextIdlePing() {
2121        long timeUntilWake = Config.IDLE_PING_INTERVAL * 1000;
2122        final var now = System.currentTimeMillis();
2123        for (final var message : mScheduledMessages.values()) {
2124            if (message.getTimeSent() <= now) continue; // Just in case
2125            if (message.getTimeSent() - now < timeUntilWake) timeUntilWake = message.getTimeSent() - now;
2126        }
2127        final var timeToWake = SystemClock.elapsedRealtime() + timeUntilWake;
2128        final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
2129        if (alarmManager == null) {
2130            Log.d(Config.LOGTAG, "no alarm manager?");
2131            return;
2132        }
2133        final Intent intent = new Intent(this, SystemEventReceiver.class);
2134        intent.setAction(ACTION_IDLE_PING);
2135        try {
2136            final PendingIntent pendingIntent =
2137                    PendingIntent.getBroadcast(
2138                            this,
2139                            0,
2140                            intent,
2141                            s()
2142                                    ? PendingIntent.FLAG_IMMUTABLE
2143                                            | PendingIntent.FLAG_UPDATE_CURRENT
2144                                    : PendingIntent.FLAG_UPDATE_CURRENT);
2145            alarmManager.setAndAllowWhileIdle(
2146                    AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
2147        } catch (RuntimeException e) {
2148            Log.d(Config.LOGTAG, "unable to schedule alarm for idle ping", e);
2149        }
2150    }
2151
2152    public XmppConnection createConnection(final Account account) {
2153        final XmppConnection connection = new XmppConnection(account, this);
2154        connection.setOnStatusChangedListener(this.statusListener);
2155        connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket));
2156        connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
2157        connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
2158        connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService);
2159        AxolotlService axolotlService = account.getAxolotlService();
2160        if (axolotlService != null) {
2161            connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
2162        }
2163        return connection;
2164    }
2165
2166    public void sendChatState(Conversation conversation) {
2167        if (sendChatStates()) {
2168            final var packet = mMessageGenerator.generateChatState(conversation);
2169            sendMessagePacket(conversation.getAccount(), packet);
2170        }
2171    }
2172
2173    private void sendFileMessage(
2174            final Message message, final boolean delay, final boolean forceP2P, final Runnable cb) {
2175        final var account = message.getConversation().getAccount();
2176        Log.d(
2177                Config.LOGTAG,
2178                account.getJid().asBareJid() + ": send file message. forceP2P=" + forceP2P);
2179        if ((account.httpUploadAvailable(fileBackend.getFile(message, false).getSize())
2180                        || message.getConversation().getMode() == Conversation.MODE_MULTI)
2181                && !forceP2P) {
2182            mHttpConnectionManager.createNewUploadConnection(message, delay, cb);
2183        } else {
2184            mJingleConnectionManager.startJingleFileTransfer(message);
2185            if (cb != null) cb.run();
2186        }
2187    }
2188
2189    public void sendMessage(final Message message) {
2190        sendMessage(message, false, false, false, false, null);
2191    }
2192
2193    public void sendMessage(final Message message, final Runnable cb) {
2194        sendMessage(message, false, false, false, false, cb);
2195    }
2196
2197    private void sendMessage(final Message message, final boolean resend, final boolean previewedLinks, final boolean delay, final Runnable cb) {
2198        sendMessage(message, resend, previewedLinks, delay, false, cb);
2199    }
2200
2201    private void sendMessage(
2202            final Message message,
2203            final boolean resend,
2204            final boolean previewedLinks,
2205            final boolean delay,
2206            final boolean forceP2P,
2207            final Runnable cb) {
2208        final Account account = message.getConversation().getAccount();
2209        if (account.setShowErrorNotification(true)) {
2210            databaseBackend.updateAccount(account);
2211            mNotificationService.updateErrorNotification();
2212        }
2213        final Conversation conversation = (Conversation) message.getConversation();
2214        account.deactivateGracePeriod();
2215
2216        if (QuickConversationsService.isQuicksy()
2217                && conversation.getMode() == Conversation.MODE_SINGLE) {
2218            final Contact contact = conversation.getContact();
2219            if (!contact.showInRoster() && contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) {
2220                Log.d(
2221                        Config.LOGTAG,
2222                        account.getJid().asBareJid()
2223                                + ": adding "
2224                                + contact.getJid()
2225                                + " on sending message");
2226                createContact(contact, true);
2227            }
2228        }
2229
2230        im.conversations.android.xmpp.model.stanza.Message packet = null;
2231        final boolean addToConversation = !message.edited() && message.getRawBody() != null;
2232        boolean saveInDb = addToConversation;
2233        message.setStatus(Message.STATUS_WAITING);
2234
2235        if (message.getEncryption() != Message.ENCRYPTION_NONE
2236                && conversation.getMode() == Conversation.MODE_MULTI
2237                && conversation.isPrivateAndNonAnonymous()) {
2238            if (conversation.setAttribute(
2239                    Conversation.ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, true)) {
2240                databaseBackend.updateConversation(conversation);
2241            }
2242        }
2243
2244        final boolean inProgressJoin = isJoinInProgress(conversation);
2245
2246        if (message.getCounterpart() == null && !message.isPrivateMessage()) {
2247            message.setCounterpart(message.getConversation().getJid().asBareJid());
2248        }
2249
2250        boolean waitForPreview = false;
2251        if (getPreferences().getBoolean("send_link_previews", true) && !previewedLinks && !message.needsUploading() && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
2252            message.clearLinkDescriptions();
2253            final List<URI> links = message.getLinks();
2254            if (!links.isEmpty()) {
2255                waitForPreview = true;
2256                if (account.isOnlineAndConnected()) {
2257                    FILE_ATTACHMENT_EXECUTOR.execute(() -> {
2258                        for (URI link : links) {
2259                            if ("https".equals(link.getScheme())) {
2260                                try {
2261                                    HttpUrl url = HttpUrl.parse(link.toString());
2262                                    OkHttpClient http = getHttpConnectionManager().buildHttpClient(url, account, 5, false);
2263                                    final var request = new okhttp3.Request.Builder().url(url).head().build();
2264                                    okhttp3.Response response = null;
2265                                    if ("www.amazon.com".equals(link.getHost()) || "www.amazon.ca".equals(link.getHost())) {
2266                                        // Amazon blocks HEAD
2267                                        response = new okhttp3.Response.Builder().request(request).protocol(okhttp3.Protocol.HTTP_1_1).code(200).message("OK").addHeader("Content-Type", "text/html").build();
2268                                    } else {
2269                                        response = http.newCall(request).execute();
2270                                    }
2271                                    final String mimeType = response.header("Content-Type") == null ? "" : response.header("Content-Type");
2272                                    final boolean image = mimeType.startsWith("image/");
2273                                    final boolean audio = mimeType.startsWith("audio/");
2274                                    final boolean video = mimeType.startsWith("video/");
2275                                    final boolean pdf = mimeType.equals("application/pdf");
2276                                    final boolean html = mimeType.startsWith("text/html") || mimeType.startsWith("application/xhtml+xml");
2277                                    if (response.isSuccessful() && (image || audio || video || pdf)) {
2278                                        Message.FileParams params = message.getFileParams();
2279                                        params.url = url.toString();
2280                                        if (response.header("Content-Length") != null) params.size = Long.parseLong(response.header("Content-Length"), 10);
2281                                        if (!Message.configurePrivateFileMessage(message)) {
2282                                            message.setType(image ? Message.TYPE_IMAGE : Message.TYPE_FILE);
2283                                        }
2284                                        params.setName(HttpConnectionManager.extractFilenameFromResponse(response));
2285
2286                                        if (link.toString().equals(message.getRawBody())) {
2287                                            Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
2288                                            fallback.addChild("body", "urn:xmpp:fallback:0");
2289                                            message.addPayload(fallback);
2290                                        } else if (message.getRawBody().indexOf(link.toString()) >= 0) {
2291                                            // Part of the real body, not just a fallback
2292                                            Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
2293                                            fallback.addChild("body", "urn:xmpp:fallback:0")
2294                                                .setAttribute("start", "0")
2295                                                .setAttribute("end", "0");
2296                                            message.addPayload(fallback);
2297                                        }
2298
2299                                        final int encryption = message.getEncryption();
2300                                        getHttpConnectionManager().createNewDownloadConnection(message, false, (file) -> {
2301                                            message.setEncryption(encryption);
2302                                            synchronized (message.getConversation()) {
2303                                                if (message.getStatus() == Message.STATUS_WAITING) sendMessage(message, true, true, false, cb);
2304                                            }
2305                                        });
2306                                        return;
2307                                    } else if (response.isSuccessful() && html) {
2308                                        Semaphore waiter = new Semaphore(0);
2309                                        OpenGraphParser.Builder openGraphBuilder = new OpenGraphParser.Builder(new OpenGraphCallback() {
2310                                            @Override
2311                                            public void onPostResponse(OpenGraphResult result) {
2312                                                Element rdf = new Element("Description", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
2313                                                rdf.setAttribute("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
2314                                                rdf.setAttribute("rdf:about", link.toString());
2315                                                if (result.getTitle() != null && !"".equals(result.getTitle())) {
2316                                                    rdf.addChild("title", "https://ogp.me/ns#").setContent(result.getTitle());
2317                                                }
2318                                                if (result.getDescription() != null && !"".equals(result.getDescription())) {
2319                                                    rdf.addChild("description", "https://ogp.me/ns#").setContent(result.getDescription());
2320                                                }
2321                                                if (result.getUrl() != null) {
2322                                                    rdf.addChild("url", "https://ogp.me/ns#").setContent(result.getUrl());
2323                                                }
2324                                                if (result.getImage() != null) {
2325                                                    rdf.addChild("image", "https://ogp.me/ns#").setContent(result.getImage());
2326                                                }
2327                                                if (result.getType() != null) {
2328                                                    rdf.addChild("type", "https://ogp.me/ns#").setContent(result.getType());
2329                                                }
2330                                                if (result.getSiteName() != null) {
2331                                                    rdf.addChild("site_name", "https://ogp.me/ns#").setContent(result.getSiteName());
2332                                                }
2333                                                if (result.getVideo() != null) {
2334                                                    rdf.addChild("video", "https://ogp.me/ns#").setContent(result.getVideo());
2335                                                }
2336                                                message.addPayload(rdf);
2337                                                waiter.release();
2338                                            }
2339
2340                                            public void onError(String error) {
2341                                                waiter.release();
2342                                            }
2343                                        })
2344                                            .showNullOnEmpty(true)
2345                                            .maxBodySize(90000)
2346                                            .timeout(5000);
2347                                        if (useTorToConnect()) {
2348                                            openGraphBuilder = openGraphBuilder.jsoupProxy(new JsoupProxy("127.0.0.1", 8118));
2349                                        }
2350                                        openGraphBuilder.build().parse(link.toString());
2351                                        waiter.tryAcquire(10L, TimeUnit.SECONDS);
2352                                    }
2353                                } catch (final IOException | InterruptedException e) {  }
2354                            }
2355                        }
2356                        synchronized (message.getConversation()) {
2357                            if (message.getStatus() == Message.STATUS_WAITING) sendMessage(message, true, true, false, cb);
2358                        }
2359                    });
2360                }
2361            }
2362        }
2363
2364        boolean passedCbOn = false;
2365        if (account.isOnlineAndConnected() && !inProgressJoin && !waitForPreview && message.getTimeSent() <= System.currentTimeMillis()) {
2366            switch (message.getEncryption()) {
2367                case Message.ENCRYPTION_NONE:
2368                    if (message.needsUploading()) {
2369                        if (account.httpUploadAvailable(
2370                                        fileBackend.getFile(message, false).getSize())
2371                                || conversation.getMode() == Conversation.MODE_MULTI
2372                                || message.fixCounterpart()) {
2373                            this.sendFileMessage(message, delay, forceP2P, cb);
2374                            passedCbOn = true;
2375                        } else {
2376                            break;
2377                        }
2378                    } else {
2379                        packet = mMessageGenerator.generateChat(message);
2380                    }
2381                    break;
2382                case Message.ENCRYPTION_PGP:
2383                case Message.ENCRYPTION_DECRYPTED:
2384                    if (message.needsUploading()) {
2385                        if (account.httpUploadAvailable(
2386                                        fileBackend.getFile(message, false).getSize())
2387                                || conversation.getMode() == Conversation.MODE_MULTI
2388                                || message.fixCounterpart()) {
2389                            this.sendFileMessage(message, delay, forceP2P, cb);
2390                            passedCbOn = true;
2391                        } else {
2392                            break;
2393                        }
2394                    } else {
2395                        packet = mMessageGenerator.generatePgpChat(message);
2396                    }
2397                    break;
2398                case Message.ENCRYPTION_AXOLOTL:
2399                    message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
2400                    if (message.needsUploading()) {
2401                        if (account.httpUploadAvailable(
2402                                        fileBackend.getFile(message, false).getSize())
2403                                || conversation.getMode() == Conversation.MODE_MULTI
2404                                || message.fixCounterpart()) {
2405                            this.sendFileMessage(message, delay, forceP2P, cb);
2406                            passedCbOn = true;
2407                        } else {
2408                            break;
2409                        }
2410                    } else {
2411                        XmppAxolotlMessage axolotlMessage =
2412                                account.getAxolotlService().fetchAxolotlMessageFromCache(message);
2413                        if (axolotlMessage == null) {
2414                            account.getAxolotlService().preparePayloadMessage(message, delay);
2415                        } else {
2416                            packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
2417                        }
2418                    }
2419                    break;
2420            }
2421            if (packet != null) {
2422                if (account.getXmppConnection().getFeatures().sm()
2423                        || (conversation.getMode() == Conversation.MODE_MULTI
2424                                && message.getCounterpart().isBareJid())) {
2425                    message.setStatus(Message.STATUS_UNSEND);
2426                } else {
2427                    message.setStatus(Message.STATUS_SEND);
2428                }
2429            }
2430        } else {
2431            switch (message.getEncryption()) {
2432                case Message.ENCRYPTION_DECRYPTED:
2433                    if (!message.needsUploading()) {
2434                        String pgpBody = message.getEncryptedBody();
2435                        String decryptedBody = message.getBody();
2436                        message.setBody(pgpBody); // TODO might throw NPE
2437                        message.setEncryption(Message.ENCRYPTION_PGP);
2438                        if (message.edited()) {
2439                            message.setBody(decryptedBody);
2440                            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
2441                            if (!databaseBackend.updateMessage(message, message.getEditedId())) {
2442                                Log.e(Config.LOGTAG, "error updated message in DB after edit");
2443                            }
2444                            updateConversationUi();
2445                            if (!waitForPreview && cb != null) cb.run();
2446                            return;
2447                        } else {
2448                            databaseBackend.createMessage(message);
2449                            saveInDb = false;
2450                            message.setBody(decryptedBody);
2451                            message.setEncryption(Message.ENCRYPTION_DECRYPTED);
2452                        }
2453                    }
2454                    break;
2455                case Message.ENCRYPTION_AXOLOTL:
2456                    message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
2457                    break;
2458            }
2459        }
2460
2461        synchronized (mScheduledMessages) {
2462            if (message.getTimeSent() > System.currentTimeMillis()) {
2463                mScheduledMessages.put(message.getUuid(), message);
2464                scheduleNextIdlePing();
2465            } else {
2466                mScheduledMessages.remove(message.getUuid());
2467            }
2468        }
2469
2470        boolean mucMessage =
2471                conversation.getMode() == Conversation.MODE_MULTI && !message.isPrivateMessage();
2472        if (mucMessage) {
2473            message.setCounterpart(conversation.getMucOptions().getSelf().getFullJid());
2474        }
2475
2476        if (resend) {
2477            if (packet != null && addToConversation) {
2478                if (account.getXmppConnection().getFeatures().sm() || mucMessage) {
2479                    markMessage(message, Message.STATUS_UNSEND);
2480                } else {
2481                    markMessage(message, Message.STATUS_SEND);
2482                }
2483            }
2484        } else {
2485            if (addToConversation) {
2486                conversation.add(message);
2487            }
2488            if (saveInDb) {
2489                databaseBackend.createMessage(message);
2490            } else if (message.edited()) {
2491                if (!databaseBackend.updateMessage(message, message.getEditedId())) {
2492                    Log.e(Config.LOGTAG, "error updated message in DB after edit");
2493                }
2494            }
2495            updateConversationUi();
2496        }
2497        if (packet != null) {
2498            if (delay) {
2499                mMessageGenerator.addDelay(packet, message.getTimeSent());
2500            }
2501            if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
2502                if (this.sendChatStates()) {
2503                    packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
2504                }
2505            }
2506            sendMessagePacket(account, packet);
2507            if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.hasCustomEmoji()) {
2508                if (message.getConversation() instanceof Conversation) presenceToMuc((Conversation) message.getConversation());
2509            }
2510        }
2511        if (!waitForPreview && !passedCbOn && cb != null) cb.run();
2512    }
2513
2514    private boolean isJoinInProgress(final Conversation conversation) {
2515        final Account account = conversation.getAccount();
2516        synchronized (account.inProgressConferenceJoins) {
2517            if (conversation.getMode() == Conversational.MODE_MULTI) {
2518                final boolean inProgress = account.inProgressConferenceJoins.contains(conversation);
2519                final boolean pending = account.pendingConferenceJoins.contains(conversation);
2520                final boolean inProgressJoin = inProgress || pending;
2521                if (inProgressJoin) {
2522                    Log.d(
2523                            Config.LOGTAG,
2524                            account.getJid().asBareJid()
2525                                    + ": holding back message to group. inProgress="
2526                                    + inProgress
2527                                    + ", pending="
2528                                    + pending);
2529                }
2530                return inProgressJoin;
2531            } else {
2532                return false;
2533            }
2534        }
2535    }
2536
2537    private void sendUnsentMessages(final Conversation conversation) {
2538        synchronized (conversation) {
2539            conversation.findWaitingMessages(message -> resendMessage(message, true));
2540        }
2541    }
2542
2543    public void resendMessage(final Message message, final boolean delay) {
2544        sendMessage(message, true, false, delay, false, null);
2545    }
2546
2547    public void resendMessage(final Message message, final boolean delay, final Runnable cb) {
2548        sendMessage(message, true, false, delay, false, cb);
2549    }
2550
2551    public void resendMessage(final Message message, final boolean delay, final boolean previewedLinks) {
2552        sendMessage(message, true, previewedLinks, delay, false, null);
2553    }
2554
2555    public Pair<Account,Account> onboardingIncomplete() {
2556        if (getAccounts().size() != 2) return null;
2557        Account onboarding = null;
2558        Account newAccount = null;
2559        for (final Account account : getAccounts()) {
2560            if (account.getJid().getDomain().equals(Config.ONBOARDING_DOMAIN)) {
2561                onboarding = account;
2562            } else {
2563                newAccount = account;
2564            }
2565        }
2566
2567        if (onboarding != null && newAccount != null) {
2568            return new Pair<>(onboarding, newAccount);
2569        }
2570
2571        return null;
2572    }
2573
2574    public boolean isOnboarding() {
2575        return getAccounts().size() == 1 && getAccounts().get(0).getJid().getDomain().equals(Config.ONBOARDING_DOMAIN);
2576    }
2577
2578    public void requestEasyOnboardingInvite(
2579            final Account account, final EasyOnboardingInvite.OnInviteRequested callback) {
2580        final XmppConnection connection = account.getXmppConnection();
2581        final Jid jid =
2582                connection == null
2583                        ? null
2584                        : connection.getJidForCommand(Namespace.EASY_ONBOARDING_INVITE);
2585        if (jid == null) {
2586            callback.inviteRequestFailed(
2587                    getString(R.string.server_does_not_support_easy_onboarding_invites));
2588            return;
2589        }
2590        final Iq request = new Iq(Iq.Type.SET);
2591        request.setTo(jid);
2592        final Element command = request.addChild("command", Namespace.COMMANDS);
2593        command.setAttribute("node", Namespace.EASY_ONBOARDING_INVITE);
2594        command.setAttribute("action", "execute");
2595        sendIqPacket(
2596                account,
2597                request,
2598                (response) -> {
2599                    if (response.getType() == Iq.Type.RESULT) {
2600                        final Element resultCommand =
2601                                response.findChild("command", Namespace.COMMANDS);
2602                        final Element x =
2603                                resultCommand == null
2604                                        ? null
2605                                        : resultCommand.findChild("x", Namespace.DATA);
2606                        if (x != null) {
2607                            final Data data = Data.parse(x);
2608                            final String uri = data.getValue("uri");
2609                            final String landingUrl = data.getValue("landing-url");
2610                            if (uri != null) {
2611                                final EasyOnboardingInvite invite =
2612                                        new EasyOnboardingInvite(
2613                                                jid.getDomain().toString(), uri, landingUrl);
2614                                callback.inviteRequested(invite);
2615                                return;
2616                            }
2617                        }
2618                        callback.inviteRequestFailed(getString(R.string.unable_to_parse_invite));
2619                        Log.d(Config.LOGTAG, response.toString());
2620                    } else if (response.getType() == Iq.Type.ERROR) {
2621                        callback.inviteRequestFailed(IqParser.errorMessage(response));
2622                    } else {
2623                        callback.inviteRequestFailed(getString(R.string.remote_server_timeout));
2624                    }
2625                });
2626    }
2627
2628    public void fetchBookmarks(final Account account) {
2629        final Iq iqPacket = new Iq(Iq.Type.GET);
2630        iqPacket.addExtension(new PrivateStorage()).addExtension(new Storage());
2631        final Consumer<Iq> callback =
2632                (response) -> {
2633                    if (response.getType() == Iq.Type.RESULT) {
2634                        final var privateStorage = response.getExtension(PrivateStorage.class);
2635                        if (privateStorage == null) {
2636                            return;
2637                        }
2638                        final var bookmarkStorage = privateStorage.getExtension(Storage.class);
2639                        Map<Jid, Bookmark> bookmarks =
2640                                Bookmark.parseFromStorage(bookmarkStorage, account);
2641                        processBookmarksInitial(account, bookmarks, false);
2642                    } else {
2643                        Log.d(
2644                                Config.LOGTAG,
2645                                account.getJid().asBareJid() + ": could not fetch bookmarks");
2646                    }
2647                };
2648        sendIqPacket(account, iqPacket, callback);
2649    }
2650
2651    public void fetchBookmarks2(final Account account) {
2652        final Iq retrieve = mIqGenerator.retrieveBookmarks();
2653        sendIqPacket(
2654                account,
2655                retrieve,
2656                (response) -> {
2657                    if (response.getType() == Iq.Type.RESULT) {
2658                        final var pubsub = response.getExtension(PubSub.class);
2659                        final Map<Jid, Bookmark> bookmarks =
2660                                Bookmark.parseFromPubSub(pubsub, account);
2661                        processBookmarksInitial(account, bookmarks, true);
2662                    }
2663                });
2664    }
2665
2666    public void fetchMessageDisplayedSynchronization(final Account account) {
2667        Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds");
2668        final var retrieve = mIqGenerator.retrieveMds();
2669        sendIqPacket(
2670                account,
2671                retrieve,
2672                (response) -> {
2673                    if (response.getType() != Iq.Type.RESULT) {
2674                        return;
2675                    }
2676                    final var pubsub = response.getExtension(PubSub.class);
2677                    if (pubsub == null) {
2678                        return;
2679                    }
2680                    final var items = pubsub.getItems();
2681                    if (items == null) {
2682                        return;
2683                    }
2684                    if (Namespace.MDS_DISPLAYED.equals(items.getNode())) {
2685                        for (final var item :
2686                                items.getItemMap(
2687                                                im.conversations.android.xmpp.model.mds.Displayed
2688                                                        .class)
2689                                        .entrySet()) {
2690                            processMdsItem(account, item);
2691                        }
2692                    }
2693                });
2694    }
2695
2696    public void processMdsItem(final Account account, final Map.Entry<String, Displayed> item) {
2697        final Jid jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(item.getKey()));
2698        if (jid == null) {
2699            return;
2700        }
2701        final var displayed = item.getValue();
2702        final var stanzaId = displayed.getStanzaId();
2703        final String id = stanzaId == null ? null : stanzaId.getId();
2704        final Conversation conversation = find(account, jid);
2705        if (id != null && conversation != null) {
2706            conversation.setDisplayState(id);
2707            markReadUpToStanzaId(conversation, id);
2708        }
2709    }
2710
2711    public void markReadUpToStanzaId(final Conversation conversation, final String stanzaId) {
2712        final Message message = conversation.findMessageWithServerMsgId(stanzaId);
2713        if (message == null) { // do we want to check if isRead?
2714            return;
2715        }
2716        markReadUpTo(conversation, message);
2717    }
2718
2719    public void markReadUpTo(final Conversation conversation, final Message message) {
2720        final boolean isDismissNotification = isDismissNotification(message);
2721        final var uuid = message.getUuid();
2722        Log.d(
2723                Config.LOGTAG,
2724                conversation.getAccount().getJid().asBareJid()
2725                        + ": mark "
2726                        + conversation.getJid().asBareJid()
2727                        + " as read up to "
2728                        + uuid);
2729        markRead(conversation, uuid, isDismissNotification);
2730    }
2731
2732    private static boolean isDismissNotification(final Message message) {
2733        Message next = message.next();
2734        while (next != null) {
2735            if (message.getStatus() == Message.STATUS_RECEIVED) {
2736                return false;
2737            }
2738            next = next.next();
2739        }
2740        return true;
2741    }
2742
2743    public void processBookmarksInitial(
2744            final Account account, final Map<Jid, Bookmark> bookmarks, final boolean pep) {
2745        final Set<Jid> previousBookmarks = account.getBookmarkedJids();
2746        for (final Bookmark bookmark : bookmarks.values()) {
2747            previousBookmarks.remove(bookmark.getJid().asBareJid());
2748            processModifiedBookmark(bookmark, pep);
2749        }
2750        if (pep) {
2751            processDeletedBookmarks(account, previousBookmarks);
2752        }
2753        account.setBookmarks(bookmarks);
2754    }
2755
2756    public void processDeletedBookmarks(final Account account, final Collection<Jid> bookmarks) {
2757        Log.d(
2758                Config.LOGTAG,
2759                account.getJid().asBareJid()
2760                        + ": "
2761                        + bookmarks.size()
2762                        + " bookmarks have been removed");
2763        for (final Jid bookmark : bookmarks) {
2764            processDeletedBookmark(account, bookmark);
2765        }
2766    }
2767
2768    public void processDeletedBookmark(final Account account, final Jid jid) {
2769        final Conversation conversation = find(account, jid);
2770        if (conversation == null) {
2771            return;
2772        }
2773        Log.d(
2774                Config.LOGTAG,
2775                account.getJid().asBareJid() + ": archiving MUC " + jid + " after PEP update");
2776        archiveConversation(conversation, false);
2777    }
2778
2779    private void processModifiedBookmark(final Bookmark bookmark, final boolean pep) {
2780        final Account account = bookmark.getAccount();
2781        Conversation conversation = find(bookmark);
2782        if (conversation != null) {
2783            if (conversation.getMode() != Conversation.MODE_MULTI) {
2784                return;
2785            }
2786            bookmark.setConversation(conversation);
2787            if (pep && !bookmark.autojoin()) {
2788                Log.d(
2789                        Config.LOGTAG,
2790                        account.getJid().asBareJid()
2791                                + ": archiving conference ("
2792                                + conversation.getJid()
2793                                + ") after receiving pep");
2794                archiveConversation(conversation, false);
2795            } else {
2796                final MucOptions mucOptions = conversation.getMucOptions();
2797                if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) {
2798                    final String current = mucOptions.getActualNick();
2799                    final String proposed = mucOptions.getProposedNickPure();
2800                    if (current != null && !current.equals(proposed)) {
2801                        Log.d(
2802                                Config.LOGTAG,
2803                                account.getJid().asBareJid()
2804                                        + ": proposed nick changed after bookmark push "
2805                                        + current
2806                                        + "->"
2807                                        + proposed);
2808                        joinMuc(conversation);
2809                    }
2810                } else {
2811                    checkMucRequiresRename(conversation);
2812                }
2813            }
2814        } else if (bookmark.autojoin()) {
2815            conversation =
2816                    findOrCreateConversation(account, bookmark.getFullJid(), true, true, false);
2817            bookmark.setConversation(conversation);
2818        }
2819    }
2820
2821    public void processModifiedBookmark(final Bookmark bookmark) {
2822        processModifiedBookmark(bookmark, true);
2823    }
2824
2825    public void ensureBookmarkIsAutoJoin(final Conversation conversation) {
2826        final var account = conversation.getAccount();
2827        final var existingBookmark = conversation.getBookmark();
2828        if (existingBookmark == null) {
2829            final var bookmark = new Bookmark(account, conversation.getJid().asBareJid());
2830            bookmark.setAutojoin(true);
2831            createBookmark(account, bookmark);
2832        } else {
2833            if (existingBookmark.autojoin()) {
2834                return;
2835            }
2836            existingBookmark.setAutojoin(true);
2837            createBookmark(account, existingBookmark);
2838        }
2839    }
2840
2841    public void createBookmark(final Account account, final Bookmark bookmark) {
2842        account.putBookmark(bookmark);
2843        final XmppConnection connection = account.getXmppConnection();
2844        if (connection == null) {
2845            Log.d(
2846                    Config.LOGTAG,
2847                    account.getJid().asBareJid() + ": no connection. ignoring bookmark creation");
2848        } else if (connection.getFeatures().bookmarks2()) {
2849            Log.d(
2850                    Config.LOGTAG,
2851                    account.getJid().asBareJid() + ": pushing bookmark via Bookmarks 2");
2852            final Element item = mIqGenerator.publishBookmarkItem(bookmark);
2853            pushNodeAndEnforcePublishOptions(
2854                    account,
2855                    Namespace.BOOKMARKS2,
2856                    item,
2857                    bookmark.getJid().asBareJid().toString(),
2858                    PublishOptions.persistentWhitelistAccessMaxItems());
2859        } else if (connection.getFeatures().bookmarksConversion()) {
2860            pushBookmarksPep(account);
2861        } else {
2862            pushBookmarksPrivateXml(account);
2863        }
2864    }
2865
2866    public void deleteBookmark(final Account account, final Bookmark bookmark) {
2867        if (bookmark.getJid().toString().equals("discuss@conference.soprani.ca")) {
2868            getPreferences().edit().putBoolean("cheogram_sopranica_bookmark_deleted", true).apply();
2869        }
2870        account.removeBookmark(bookmark);
2871        final XmppConnection connection = account.getXmppConnection();
2872        if (connection == null) return;
2873
2874        if (connection.getFeatures().bookmarks2()) {
2875            final Iq request =
2876                    mIqGenerator.deleteItem(
2877                            Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toString());
2878            Log.d(
2879                    Config.LOGTAG,
2880                    account.getJid().asBareJid() + ": removing bookmark via Bookmarks 2");
2881            sendIqPacket(
2882                    account,
2883                    request,
2884                    (response) -> {
2885                        if (response.getType() == Iq.Type.ERROR) {
2886                            Log.d(
2887                                    Config.LOGTAG,
2888                                    account.getJid().asBareJid()
2889                                            + ": unable to delete bookmark "
2890                                            + response.getErrorCondition());
2891                        }
2892                    });
2893        } else if (connection.getFeatures().bookmarksConversion()) {
2894            pushBookmarksPep(account);
2895        } else {
2896            pushBookmarksPrivateXml(account);
2897        }
2898    }
2899
2900    private void pushBookmarksPrivateXml(Account account) {
2901        if (!account.areBookmarksLoaded()) return;
2902
2903        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml");
2904        final Iq iqPacket = new Iq(Iq.Type.SET);
2905        Element query = iqPacket.query("jabber:iq:private");
2906        Element storage = query.addChild("storage", "storage:bookmarks");
2907        for (final Bookmark bookmark : account.getBookmarks()) {
2908            storage.addChild(bookmark);
2909        }
2910        sendIqPacket(account, iqPacket, mDefaultIqHandler);
2911    }
2912
2913    private void pushBookmarksPep(Account account) {
2914        if (!account.areBookmarksLoaded()) return;
2915
2916        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via pep");
2917        final Element storage = new Element("storage", "storage:bookmarks");
2918        for (final Bookmark bookmark : account.getBookmarks()) {
2919            storage.addChild(bookmark);
2920        }
2921        pushNodeAndEnforcePublishOptions(
2922                account,
2923                Namespace.BOOKMARKS,
2924                storage,
2925                "current",
2926                PublishOptions.persistentWhitelistAccess());
2927    }
2928
2929    private void pushNodeAndEnforcePublishOptions(
2930            final Account account,
2931            final String node,
2932            final Element element,
2933            final String id,
2934            final Bundle options) {
2935        pushNodeAndEnforcePublishOptions(account, node, element, id, options, true);
2936    }
2937
2938    private void pushNodeAndEnforcePublishOptions(
2939            final Account account,
2940            final String node,
2941            final Element element,
2942            final String id,
2943            final Bundle options,
2944            final boolean retry) {
2945        final Iq packet = mIqGenerator.publishElement(node, element, id, options);
2946        sendIqPacket(
2947                account,
2948                packet,
2949                (response) -> {
2950                    if (response.getType() == Iq.Type.RESULT) {
2951                        return;
2952                    }
2953                    if (retry && PublishOptions.preconditionNotMet(response)) {
2954                        pushNodeConfiguration(
2955                                account,
2956                                node,
2957                                options,
2958                                new OnConfigurationPushed() {
2959                                    @Override
2960                                    public void onPushSucceeded() {
2961                                        pushNodeAndEnforcePublishOptions(
2962                                                account, node, element, id, options, false);
2963                                    }
2964
2965                                    @Override
2966                                    public void onPushFailed() {
2967                                        Log.d(
2968                                                Config.LOGTAG,
2969                                                account.getJid().asBareJid()
2970                                                        + ": unable to push node configuration ("
2971                                                        + node
2972                                                        + ")");
2973                                    }
2974                                });
2975                    } else {
2976                        Log.d(
2977                                Config.LOGTAG,
2978                                account.getJid().asBareJid()
2979                                        + ": error publishing "
2980                                        + node
2981                                        + " (retry="
2982                                        + retry
2983                                        + ") "
2984                                        + response);
2985                    }
2986                });
2987    }
2988
2989    private void restoreFromDatabase() {
2990        synchronized (this.conversations) {
2991            final Map<String, Account> accountLookupTable =
2992                    ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
2993            Log.d(Config.LOGTAG, "restoring conversations...");
2994            final long startTimeConversationsRestore = SystemClock.elapsedRealtime();
2995            this.conversations.addAll(
2996                    databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
2997            for (Iterator<Conversation> iterator = conversations.listIterator();
2998                    iterator.hasNext(); ) {
2999                Conversation conversation = iterator.next();
3000                Account account = accountLookupTable.get(conversation.getAccountUuid());
3001                if (account != null) {
3002                    conversation.setAccount(account);
3003                } else {
3004                    Log.e(Config.LOGTAG, "unable to restore Conversations with " + conversation.getJid());
3005                    conversations.remove(conversation);
3006                }
3007            }
3008            long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore;
3009            Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms");
3010            Runnable runnable = () -> {
3011                if (DatabaseBackend.requiresMessageIndexRebuild()) {
3012                    DatabaseBackend.getInstance(this).rebuildMessagesIndex();
3013                }
3014                mutedMucUsers = databaseBackend.loadMutedMucUsers();
3015                final long deletionDate = getAutomaticMessageDeletionDate();
3016                mLastExpiryRun.set(SystemClock.elapsedRealtime());
3017                if (deletionDate > 0) {
3018                    Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate));
3019                    databaseBackend.expireOldMessages(deletionDate);
3020                }
3021                Log.d(Config.LOGTAG, "restoring roster...");
3022                for (final Account account : accounts) {
3023                    databaseBackend.readRoster(account.getRoster());
3024                    account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage
3025                }
3026                getDrawableCache().evictAll();
3027                loadPhoneContacts();
3028                Log.d(Config.LOGTAG, "restoring messages...");
3029                final long startMessageRestore = SystemClock.elapsedRealtime();
3030                final Conversation quickLoad = QuickLoader.get(this.conversations);
3031                if (quickLoad != null) {
3032                    restoreMessages(quickLoad);
3033                    updateConversationUi();
3034                    final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
3035                    Log.d(Config.LOGTAG, "quickly restored " + quickLoad.getName() + " after " + diffMessageRestore + "ms");
3036                }
3037                for (Conversation conversation : this.conversations) {
3038                    if (quickLoad != conversation) {
3039                        restoreMessages(conversation);
3040                    }
3041                }
3042                mNotificationService.finishBacklog();
3043                restoredFromDatabaseLatch.countDown();
3044                final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
3045                Log.d(Config.LOGTAG, "finished restoring messages in " + diffMessageRestore + "ms");
3046                updateConversationUi();
3047            };
3048            mDatabaseReaderExecutor.execute(runnable); //will contain one write command (expiry) but that's fine
3049        }
3050    }
3051
3052    private void restoreMessages(Conversation conversation) {
3053        conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
3054        conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
3055        conversation.findMessagesAndCallsToNotify(mNotificationService::pushFromBacklog);
3056    }
3057
3058    public void loadPhoneContacts() {
3059        mContactMergerExecutor.execute(
3060                () -> {
3061                    final Map<Jid, JabberIdContact> contacts = JabberIdContact.load(this);
3062                    Log.d(Config.LOGTAG, "start merging phone contacts with roster");
3063                    for (final Account account : accounts) {
3064                        final List<Contact> withSystemAccounts =
3065                                account.getRoster().getWithSystemAccounts(JabberIdContact.class);
3066                        for (final JabberIdContact jidContact : contacts.values()) {
3067                            final Contact contact =
3068                                    account.getRoster().getContact(jidContact.getJid());
3069                            boolean needsCacheClean = contact.setPhoneContact(jidContact);
3070                            if (needsCacheClean) {
3071                                getAvatarService().clear(contact);
3072                            }
3073                            withSystemAccounts.remove(contact);
3074                        }
3075                        for (final Contact contact : withSystemAccounts) {
3076                            boolean needsCacheClean =
3077                                    contact.unsetPhoneContact(JabberIdContact.class);
3078                            if (needsCacheClean) {
3079                                getAvatarService().clear(contact);
3080                            }
3081                        }
3082                    }
3083                    Log.d(Config.LOGTAG, "finished merging phone contacts");
3084                    mShortcutService.refresh(
3085                            mInitialAddressbookSyncCompleted.compareAndSet(false, true));
3086                    updateRosterUi(UpdateRosterReason.PUSH);
3087                    mQuickConversationsService.considerSync();
3088                });
3089    }
3090
3091    public void syncRoster(final Account account) {
3092        mRosterSyncTaskManager.execute(account, () -> {
3093            unregisterPhoneAccounts(account);
3094            databaseBackend.writeRoster(account.getRoster());
3095            try { Thread.sleep(500); } catch (InterruptedException e) { }
3096        });
3097    }
3098
3099    public List<Conversation> getConversations() {
3100        return this.conversations;
3101    }
3102
3103    private void markFileDeleted(final File file) {
3104        synchronized (FILENAMES_TO_IGNORE_DELETION) {
3105            if (FILENAMES_TO_IGNORE_DELETION.remove(file.getAbsolutePath())) {
3106                Log.d(Config.LOGTAG, "ignored deletion of " + file.getAbsolutePath());
3107                return;
3108            }
3109        }
3110        final boolean isInternalFile = fileBackend.isInternalFile(file);
3111        final List<String> uuids = databaseBackend.markFileAsDeleted(file, isInternalFile);
3112        Log.d(
3113                Config.LOGTAG,
3114                "deleted file "
3115                        + file.getAbsolutePath()
3116                        + " internal="
3117                        + isInternalFile
3118                        + ", database hits="
3119                        + uuids.size());
3120        markUuidsAsDeletedFiles(uuids);
3121    }
3122
3123    private void markUuidsAsDeletedFiles(List<String> uuids) {
3124        boolean deleted = false;
3125        for (Conversation conversation : getConversations()) {
3126            deleted |= conversation.markAsDeleted(uuids);
3127        }
3128        for (final String uuid : uuids) {
3129            evictPreview(uuid);
3130        }
3131        if (deleted) {
3132            updateConversationUi();
3133        }
3134    }
3135
3136    private void markChangedFiles(List<DatabaseBackend.FilePathInfo> infos) {
3137        boolean changed = false;
3138        for (Conversation conversation : getConversations()) {
3139            changed |= conversation.markAsChanged(infos);
3140        }
3141        if (changed) {
3142            updateConversationUi();
3143        }
3144    }
3145
3146    public void populateWithOrderedConversations(final List<Conversation> list) {
3147        populateWithOrderedConversations(list, true, true);
3148    }
3149
3150    public void populateWithOrderedConversations(
3151            final List<Conversation> list, final boolean includeNoFileUpload) {
3152        populateWithOrderedConversations(list, includeNoFileUpload, true);
3153    }
3154
3155    public void populateWithOrderedConversations(
3156            final List<Conversation> list, final boolean includeNoFileUpload, final boolean sort) {
3157        final List<String> orderedUuids;
3158        if (sort) {
3159            orderedUuids = null;
3160        } else {
3161            orderedUuids = new ArrayList<>();
3162            for (Conversation conversation : list) {
3163                orderedUuids.add(conversation.getUuid());
3164            }
3165        }
3166        list.clear();
3167        if (includeNoFileUpload) {
3168            list.addAll(getConversations());
3169        } else {
3170            for (Conversation conversation : getConversations()) {
3171                if (conversation.getMode() == Conversation.MODE_SINGLE
3172                        || (conversation.getAccount().httpUploadAvailable()
3173                                && conversation.getMucOptions().participating())) {
3174                    list.add(conversation);
3175                }
3176            }
3177        }
3178        try {
3179            if (orderedUuids != null) {
3180                Collections.sort(
3181                        list,
3182                        (a, b) -> {
3183                            final int indexA = orderedUuids.indexOf(a.getUuid());
3184                            final int indexB = orderedUuids.indexOf(b.getUuid());
3185                            if (indexA == -1 || indexB == -1 || indexA == indexB) {
3186                                return a.compareTo(b);
3187                            }
3188                            return indexA - indexB;
3189                        });
3190            } else {
3191                Collections.sort(list);
3192            }
3193        } catch (IllegalArgumentException e) {
3194            // ignore
3195        }
3196    }
3197
3198    public void loadMoreMessages(
3199            final Conversation conversation,
3200            final long timestamp,
3201            final OnMoreMessagesLoaded callback) {
3202        if (XmppConnectionService.this
3203                .getMessageArchiveService()
3204                .queryInProgress(conversation, callback)) {
3205            return;
3206        } else if (timestamp == 0) {
3207            return;
3208        }
3209        Log.d(
3210                Config.LOGTAG,
3211                "load more messages for "
3212                        + conversation.getName()
3213                        + " prior to "
3214                        + MessageGenerator.getTimestamp(timestamp));
3215        final Runnable runnable =
3216                () -> {
3217                    final Account account = conversation.getAccount();
3218                    List<Message> messages =
3219                            databaseBackend.getMessages(conversation, 50, timestamp);
3220                    if (messages.size() > 0) {
3221                        conversation.addAll(0, messages);
3222                        callback.onMoreMessagesLoaded(messages.size(), conversation);
3223                    } else if (conversation.hasMessagesLeftOnServer()
3224                            && account.isOnlineAndConnected()
3225                            && conversation.getLastClearHistory().getTimestamp() == 0) {
3226                        final boolean mamAvailable;
3227                        if (conversation.getMode() == Conversation.MODE_SINGLE) {
3228                            mamAvailable =
3229                                    account.getXmppConnection().getFeatures().mam()
3230                                            && !conversation.getContact().isBlocked();
3231                        } else {
3232                            mamAvailable = conversation.getMucOptions().mamSupport();
3233                        }
3234                        if (mamAvailable) {
3235                            MessageArchiveService.Query query =
3236                                    getMessageArchiveService()
3237                                            .query(
3238                                                    conversation,
3239                                                    new MamReference(0),
3240                                                    timestamp,
3241                                                    false);
3242                            if (query != null) {
3243                                query.setCallback(callback);
3244                                callback.informUser(R.string.fetching_history_from_server);
3245                            } else {
3246                                callback.informUser(R.string.not_fetching_history_retention_period);
3247                            }
3248                        }
3249                    }
3250                };
3251        mDatabaseReaderExecutor.execute(runnable);
3252    }
3253
3254    public List<Account> getAccounts() {
3255        return this.accounts;
3256    }
3257
3258    /**
3259     * This will find all conferences with the contact as member and also the conference that is the
3260     * contact (that 'fake' contact is used to store the avatar)
3261     */
3262    public List<Conversation> findAllConferencesWith(Contact contact) {
3263        final ArrayList<Conversation> results = new ArrayList<>();
3264        for (final Conversation c : conversations) {
3265            if (c.getMode() != Conversation.MODE_MULTI) {
3266                continue;
3267            }
3268            final MucOptions mucOptions = c.getMucOptions();
3269            if (c.getJid().asBareJid().equals(contact.getJid().asBareJid())
3270                    || (mucOptions != null && mucOptions.isContactInRoom(contact))) {
3271                results.add(c);
3272            }
3273        }
3274        return results;
3275    }
3276
3277    public Conversation find(final Contact contact) {
3278        for (final Conversation conversation : this.conversations) {
3279            if (conversation.getContact() == contact) {
3280                return conversation;
3281            }
3282        }
3283        return null;
3284    }
3285
3286    public Conversation find(
3287            final Iterable<Conversation> haystack, final Account account, final Jid jid) {
3288        if (jid == null) {
3289            return null;
3290        }
3291        for (final Conversation conversation : haystack) {
3292            if ((account == null || conversation.getAccount() == account)
3293                    && (conversation.getJid().asBareJid().equals(jid.asBareJid()))) {
3294                return conversation;
3295            }
3296        }
3297        return null;
3298    }
3299
3300    public boolean isConversationsListEmpty(final Conversation ignore) {
3301        synchronized (this.conversations) {
3302            final int size = this.conversations.size();
3303            return size == 0 || size == 1 && this.conversations.get(0) == ignore;
3304        }
3305    }
3306
3307    public boolean isConversationStillOpen(final Conversation conversation) {
3308        synchronized (this.conversations) {
3309            for (Conversation current : this.conversations) {
3310                if (current == conversation) {
3311                    return true;
3312                }
3313            }
3314        }
3315        return false;
3316    }
3317
3318    public void maybeRegisterWithMuc(Conversation c, String nickArg) {
3319        final var nick = nickArg == null ? c.getMucOptions().getSelf().getFullJid().getResource() : nickArg;
3320        final var register = new Iq(Iq.Type.GET);
3321        register.query(Namespace.REGISTER);
3322        register.setTo(c.getJid().asBareJid());
3323        sendIqPacket(c.getAccount(), register, (response) -> {
3324            if (response.getType() == Iq.Type.RESULT) {
3325                final Element query = response.query(Namespace.REGISTER);
3326                String username = query.findChildContent("username", Namespace.REGISTER);
3327                if (username == null) username = query.findChildContent("nick", Namespace.REGISTER);
3328                if (username != null && username.equals(nick)) {
3329                    // Already registered with this nick, done
3330                    Log.d(Config.LOGTAG, "Already registered with " + c.getJid().asBareJid() + " as " + username);
3331                    return;
3332                }
3333                Data form = Data.parse(query.findChild("x", Namespace.DATA));
3334                if (form != null) {
3335                    final var field = form.getFieldByName("muc#register_roomnick");
3336                    if (field != null && nick.equals(field.getValue())) {
3337                        Log.d(Config.LOGTAG, "Already registered with " + c.getJid().asBareJid() + " as " + field.getValue());
3338                        return;
3339                    }
3340                }
3341                if (form == null || !"form".equals(form.getFormType()) || !form.getFields().stream().anyMatch(f -> f.isRequired() && !"muc#register_roomnick".equals(f.getFieldName()))) {
3342                    // No form, result form, or no required fields other than nickname, let's just send nickname
3343                    if (form == null || !"form".equals(form.getFormType())) {
3344                        form = new Data();
3345                        form.put("FORM_TYPE", "http://jabber.org/protocol/muc#register");
3346                    }
3347                    form.put("muc#register_roomnick", nick);
3348                    form.submit();
3349                    final var finish = new Iq(Iq.Type.SET);
3350                    finish.query(Namespace.REGISTER).addChild(form);
3351                    finish.setTo(c.getJid().asBareJid());
3352                    sendIqPacket(c.getAccount(), finish, (response2) -> {
3353                        if (response.getType() == Iq.Type.RESULT) {
3354                            Log.w(Config.LOGTAG, "Success registering with channel " + c.getJid().asBareJid() + "/" + nick);
3355                        } else {
3356                            Log.w(Config.LOGTAG, "Error registering with channel: " + response2);
3357                        }
3358                    });
3359                } else {
3360                    // TODO: offer registration form to user
3361                    Log.d(Config.LOGTAG, "Complex registration form for " + c.getJid().asBareJid() + ": " + response);
3362                }
3363            } else {
3364                // We said maybe. Guess not
3365                Log.d(Config.LOGTAG, "Could not register with " + c.getJid().asBareJid() + ": " + response);
3366            }
3367        });
3368    }
3369
3370    public void deregisterWithMuc(Conversation c) {
3371        final Iq register = new Iq(Iq.Type.GET);
3372        register.query(Namespace.REGISTER).addChild("remove");
3373        register.setTo(c.getJid().asBareJid());
3374        sendIqPacket(c.getAccount(), register, (response) -> {
3375            if (response.getType() == Iq.Type.RESULT) {
3376                Log.d(Config.LOGTAG, "deregistered with " + c.getJid().asBareJid());
3377            } else {
3378                Log.w(Config.LOGTAG, "Could not deregister with " + c.getJid().asBareJid() + ": " + response);
3379            }
3380        });
3381    }
3382
3383    public Conversation findOrCreateConversation(
3384            Account account, Jid jid, boolean muc, final boolean async) {
3385        return this.findOrCreateConversation(account, jid, muc, false, async);
3386    }
3387
3388    public Conversation findOrCreateConversation(
3389            final Account account,
3390            final Jid jid,
3391            final boolean muc,
3392            final boolean joinAfterCreate,
3393            final boolean async) {
3394        return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, null, async, null);
3395    }
3396
3397    public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final MessageArchiveService.Query query, final boolean async) {
3398        return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, query, async, null);
3399    }
3400
3401    public Conversation findOrCreateConversation(
3402            final Account account,
3403            final Jid jid,
3404            final boolean muc,
3405            final boolean joinAfterCreate,
3406            final MessageArchiveService.Query query,
3407            final boolean async,
3408            final String password) {
3409        synchronized (this.conversations) {
3410            final var cached = find(account, jid);
3411            if (cached != null) {
3412                return cached;
3413            }
3414            final var existing = databaseBackend.findConversation(account, jid);
3415            final Conversation conversation;
3416            final boolean loadMessagesFromDb;
3417            if (existing != null) {
3418                conversation = existing;
3419                if (password != null) conversation.getMucOptions().setPassword(password);
3420                loadMessagesFromDb = restoreFromArchive(conversation, jid, muc);
3421            } else {
3422                String conversationName;
3423                final Contact contact = account.getRoster().getContact(jid);
3424                if (contact != null) {
3425                    conversationName = contact.getDisplayName();
3426                } else {
3427                    conversationName = jid.getLocal();
3428                }
3429                if (muc) {
3430                    conversation =
3431                            new Conversation(
3432                                    conversationName, account, jid, Conversation.MODE_MULTI);
3433                } else {
3434                    conversation =
3435                            new Conversation(
3436                                    conversationName,
3437                                    account,
3438                                    jid.asBareJid(),
3439                                    Conversation.MODE_SINGLE);
3440                }
3441                if (password != null) conversation.getMucOptions().setPassword(password);
3442                this.databaseBackend.createConversation(conversation);
3443                loadMessagesFromDb = false;
3444            }
3445            if (async) {
3446                mDatabaseReaderExecutor.execute(
3447                        () ->
3448                                postProcessConversation(
3449                                        conversation, loadMessagesFromDb, joinAfterCreate, query));
3450            } else {
3451                postProcessConversation(conversation, loadMessagesFromDb, joinAfterCreate, query);
3452            }
3453            this.conversations.add(conversation);
3454            updateConversationUi();
3455            return conversation;
3456        }
3457    }
3458
3459    public Conversation findConversationByUuidReliable(final String uuid) {
3460        final var cached = findConversationByUuid(uuid);
3461        if (cached != null) {
3462            return cached;
3463        }
3464        final var existing = databaseBackend.findConversation(uuid);
3465        if (existing == null) {
3466            return null;
3467        }
3468        Log.d(Config.LOGTAG, "restoring conversation with " + existing.getJid() + " from DB");
3469        final Map<String, Account> accounts =
3470                ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
3471        final var account = accounts.get(existing.getAccountUuid());
3472        if (account == null) {
3473            Log.d(Config.LOGTAG, "could not find account " + existing.getAccountUuid());
3474            return null;
3475        }
3476        existing.setAccount(account);
3477        final var loadMessagesFromDb = restoreFromArchive(existing);
3478        mDatabaseReaderExecutor.execute(
3479                () ->
3480                        postProcessConversation(
3481                                existing,
3482                                loadMessagesFromDb,
3483                                existing.getMode() == Conversational.MODE_MULTI,
3484                                null));
3485        this.conversations.add(existing);
3486        if (existing.getMode() == Conversational.MODE_MULTI) {
3487            ensureBookmarkIsAutoJoin(existing);
3488        }
3489        updateConversationUi();
3490        return existing;
3491    }
3492
3493    private boolean restoreFromArchive(
3494            final Conversation conversation, final Jid jid, final boolean muc) {
3495        if (muc) {
3496            conversation.setMode(Conversation.MODE_MULTI);
3497            conversation.setContactJid(jid);
3498        } else {
3499            conversation.setMode(Conversation.MODE_SINGLE);
3500            conversation.setContactJid(jid.asBareJid());
3501        }
3502        return restoreFromArchive(conversation);
3503    }
3504
3505    private boolean restoreFromArchive(final Conversation conversation) {
3506        conversation.setStatus(Conversation.STATUS_AVAILABLE);
3507        databaseBackend.updateConversation(conversation);
3508        return conversation.messagesLoaded.compareAndSet(true, false);
3509    }
3510
3511    private void postProcessConversation(
3512            final Conversation c,
3513            final boolean loadMessagesFromDb,
3514            final boolean joinAfterCreate,
3515            final MessageArchiveService.Query query) {
3516        final var singleMode = c.getMode() == Conversational.MODE_SINGLE;
3517        final var account = c.getAccount();
3518        if (loadMessagesFromDb) {
3519            c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE));
3520            updateConversationUi();
3521            c.messagesLoaded.set(true);
3522        }
3523        if (account.getXmppConnection() != null
3524                && !c.getContact().isBlocked()
3525                && account.getXmppConnection().getFeatures().mam()
3526                && singleMode) {
3527            if (query == null) {
3528                mMessageArchiveService.query(c);
3529            } else {
3530                if (query.getConversation() == null) {
3531                    mMessageArchiveService.query(c, query.getStart(), query.isCatchup());
3532                }
3533            }
3534        }
3535        if (joinAfterCreate) {
3536            joinMuc(c);
3537        }
3538    }
3539
3540    public void archiveConversation(Conversation conversation) {
3541        archiveConversation(conversation, true);
3542    }
3543
3544    private void archiveConversation(
3545            Conversation conversation, final boolean maySynchronizeWithBookmarks) {
3546        if (isOnboarding()) return;
3547
3548        getNotificationService().clear(conversation);
3549        conversation.setStatus(Conversation.STATUS_ARCHIVED);
3550        conversation.setNextMessage(null);
3551        synchronized (this.conversations) {
3552            getMessageArchiveService().kill(conversation);
3553            if (conversation.getMode() == Conversation.MODE_MULTI) {
3554                if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
3555                    final Bookmark bookmark = conversation.getBookmark();
3556                    if (maySynchronizeWithBookmarks && bookmark != null) {
3557                        if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) {
3558                            Account account = bookmark.getAccount();
3559                            bookmark.setConversation(null);
3560                            deleteBookmark(account, bookmark);
3561                        } else if (bookmark.autojoin()) {
3562                            bookmark.setAutojoin(false);
3563                            createBookmark(bookmark.getAccount(), bookmark);
3564                        }
3565                    }
3566                }
3567                deregisterWithMuc(conversation);
3568                leaveMuc(conversation);
3569            } else {
3570                if (conversation
3571                        .getContact()
3572                        .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
3573                    stopPresenceUpdatesTo(conversation.getContact());
3574                }
3575            }
3576            updateConversation(conversation);
3577            this.conversations.remove(conversation);
3578            updateConversationUi();
3579        }
3580    }
3581
3582    public void stopPresenceUpdatesTo(Contact contact) {
3583        Log.d(Config.LOGTAG, "Canceling presence request from " + contact.getJid().toString());
3584        sendPresencePacket(contact.getAccount(), mPresenceGenerator.stopPresenceUpdatesTo(contact));
3585        contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
3586    }
3587
3588    public void createAccount(final Account account) {
3589        account.initAccountServices(this);
3590        databaseBackend.createAccount(account);
3591        if (CallIntegration.hasSystemFeature(this)) {
3592            CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
3593        }
3594        this.accounts.add(account);
3595        this.reconnectAccountInBackground(account);
3596        updateAccountUi();
3597        syncEnabledAccountSetting();
3598        toggleForegroundService();
3599    }
3600
3601    private void syncEnabledAccountSetting() {
3602        final boolean hasEnabledAccounts = hasEnabledAccounts();
3603        getPreferences()
3604                .edit()
3605                .putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts)
3606                .apply();
3607        toggleSetProfilePictureActivity(hasEnabledAccounts);
3608    }
3609
3610    private void toggleSetProfilePictureActivity(final boolean enabled) {
3611        try {
3612            final ComponentName name =
3613                    new ComponentName(this, ChooseAccountForProfilePictureActivity.class);
3614            final int targetState =
3615                    enabled
3616                            ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
3617                            : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
3618            getPackageManager()
3619                    .setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP);
3620        } catch (IllegalStateException e) {
3621            Log.d(Config.LOGTAG, "unable to toggle profile picture activity");
3622        }
3623    }
3624
3625    public boolean reconfigurePushDistributor() {
3626        return this.unifiedPushBroker.reconfigurePushDistributor();
3627    }
3628
3629    private Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints(
3630            final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger) {
3631        return this.unifiedPushBroker.renewUnifiedPushEndpoints(pushTargetMessenger);
3632    }
3633
3634    public Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints() {
3635        return this.unifiedPushBroker.renewUnifiedPushEndpoints(null);
3636    }
3637
3638    public UnifiedPushBroker getUnifiedPushBroker() {
3639        return this.unifiedPushBroker;
3640    }
3641
3642    private void provisionAccount(final String address, final String password) {
3643        final Jid jid = Jid.of(address);
3644        final Account account = new Account(jid, password);
3645        account.setOption(Account.OPTION_DISABLED, true);
3646        Log.d(Config.LOGTAG, jid.asBareJid().toString() + ": provisioning account");
3647        createAccount(account);
3648    }
3649
3650    public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
3651        new Thread(
3652                        () -> {
3653                            try {
3654                                final X509Certificate[] chain =
3655                                        KeyChain.getCertificateChain(this, alias);
3656                                final X509Certificate cert =
3657                                        chain != null && chain.length > 0 ? chain[0] : null;
3658                                if (cert == null) {
3659                                    callback.informUser(R.string.unable_to_parse_certificate);
3660                                    return;
3661                                }
3662                                Pair<Jid, String> info = CryptoHelper.extractJidAndName(cert);
3663                                if (info == null) {
3664                                    callback.informUser(R.string.certificate_does_not_contain_jid);
3665                                    return;
3666                                }
3667                                if (findAccountByJid(info.first) == null) {
3668                                    final Account account = new Account(info.first, "");
3669                                    account.setPrivateKeyAlias(alias);
3670                                    account.setOption(Account.OPTION_DISABLED, true);
3671                                    account.setOption(Account.OPTION_FIXED_USERNAME, true);
3672                                    account.setDisplayName(info.second);
3673                                    createAccount(account);
3674                                    callback.onAccountCreated(account);
3675                                    if (Config.X509_VERIFICATION) {
3676                                        try {
3677                                            getMemorizingTrustManager()
3678                                                    .getNonInteractive(account.getServer(), null, 0, null)
3679                                                    .checkClientTrusted(chain, "RSA");
3680                                        } catch (CertificateException e) {
3681                                            callback.informUser(
3682                                                    R.string.certificate_chain_is_not_trusted);
3683                                        }
3684                                    }
3685                                } else {
3686                                    callback.informUser(R.string.account_already_exists);
3687                                }
3688                            } catch (Exception e) {
3689                                callback.informUser(R.string.unable_to_parse_certificate);
3690                            }
3691                        })
3692                .start();
3693    }
3694
3695    public void updateKeyInAccount(final Account account, final String alias) {
3696        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": update key in account " + alias);
3697        try {
3698            X509Certificate[] chain =
3699                    KeyChain.getCertificateChain(XmppConnectionService.this, alias);
3700            Log.d(Config.LOGTAG, account.getJid().asBareJid() + " loaded certificate chain");
3701            Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
3702            if (info == null) {
3703                showErrorToastInUi(R.string.certificate_does_not_contain_jid);
3704                return;
3705            }
3706            if (account.getJid().asBareJid().equals(info.first)) {
3707                account.setPrivateKeyAlias(alias);
3708                account.setDisplayName(info.second);
3709                databaseBackend.updateAccount(account);
3710                if (Config.X509_VERIFICATION) {
3711                    try {
3712                        getMemorizingTrustManager()
3713                                .getNonInteractive()
3714                                .checkClientTrusted(chain, "RSA");
3715                    } catch (CertificateException e) {
3716                        showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
3717                    }
3718                    account.getAxolotlService().regenerateKeys(true);
3719                }
3720            } else {
3721                showErrorToastInUi(R.string.jid_does_not_match_certificate);
3722            }
3723        } catch (Exception e) {
3724            e.printStackTrace();
3725        }
3726    }
3727
3728    public boolean updateAccount(final Account account) {
3729        if (databaseBackend.updateAccount(account)) {
3730            Integer color = account.getColorToSave();
3731            if (color == null) {
3732                getPreferences().edit().remove("account_color:" + account.getUuid()).commit();
3733            } else {
3734                getPreferences().edit().putInt("account_color:" + account.getUuid(), color.intValue()).commit();
3735            }
3736            account.setShowErrorNotification(true);
3737            this.statusListener.onStatusChanged(account);
3738            databaseBackend.updateAccount(account);
3739            reconnectAccountInBackground(account);
3740            updateAccountUi();
3741            getNotificationService().updateErrorNotification();
3742            toggleForegroundService();
3743            syncEnabledAccountSetting();
3744            mChannelDiscoveryService.cleanCache();
3745            if (CallIntegration.hasSystemFeature(this)) {
3746                CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
3747            }
3748            return true;
3749        } else {
3750            return false;
3751        }
3752    }
3753
3754    public void updateAccountPasswordOnServer(
3755            final Account account,
3756            final String newPassword,
3757            final OnAccountPasswordChanged callback) {
3758        final Iq iq = getIqGenerator().generateSetPassword(account, newPassword);
3759        sendIqPacket(
3760                account,
3761                iq,
3762                (packet) -> {
3763                    if (packet.getType() == Iq.Type.RESULT) {
3764                        account.setPassword(newPassword);
3765                        account.setOption(Account.OPTION_MAGIC_CREATE, false);
3766                        databaseBackend.updateAccount(account);
3767                        callback.onPasswordChangeSucceeded();
3768                    } else {
3769                        callback.onPasswordChangeFailed();
3770                    }
3771                });
3772    }
3773
3774    public void unregisterAccount(final Account account, final Consumer<Boolean> callback) {
3775        final Iq iqPacket = new Iq(Iq.Type.SET);
3776        final Element query = iqPacket.addChild("query", Namespace.REGISTER);
3777        query.addChild("remove");
3778        sendIqPacket(
3779                account,
3780                iqPacket,
3781                (response) -> {
3782                    if (response.getType() == Iq.Type.RESULT) {
3783                        deleteAccount(account);
3784                        callback.accept(true);
3785                    } else {
3786                        callback.accept(false);
3787                    }
3788                });
3789    }
3790
3791    public void deleteAccount(final Account account) {
3792        getPreferences().edit().remove("onboarding_continued").commit();
3793        final boolean connected = account.getStatus() == Account.State.ONLINE;
3794        synchronized (this.conversations) {
3795            if (connected) {
3796                account.getAxolotlService().deleteOmemoIdentity();
3797            }
3798            for (final Conversation conversation : conversations) {
3799                if (conversation.getAccount() == account) {
3800                    if (conversation.getMode() == Conversation.MODE_MULTI) {
3801                        if (connected) {
3802                            leaveMuc(conversation);
3803                        }
3804                    }
3805                    conversations.remove(conversation);
3806                    mNotificationService.clear(conversation);
3807                }
3808            }
3809            new Thread(() -> {
3810                for (final Contact contact : account.getRoster().getContacts()) {
3811                    contact.unregisterAsPhoneAccount(this);
3812                }
3813            }).start();
3814            if (account.getXmppConnection() != null) {
3815                new Thread(() -> disconnect(account, !connected)).start();
3816            }
3817            final Runnable runnable =
3818                    () -> {
3819                        if (!databaseBackend.deleteAccount(account)) {
3820                            Log.d(
3821                                    Config.LOGTAG,
3822                                    account.getJid().asBareJid() + ": unable to delete account");
3823                        }
3824                    };
3825            mDatabaseWriterExecutor.execute(runnable);
3826            this.accounts.remove(account);
3827            if (CallIntegration.hasSystemFeature(this)) {
3828                CallIntegrationConnectionService.unregisterPhoneAccount(this, account);
3829            }
3830            this.mRosterSyncTaskManager.clear(account);
3831            updateAccountUi();
3832            mNotificationService.updateErrorNotification();
3833            syncEnabledAccountSetting();
3834            toggleForegroundService();
3835        }
3836    }
3837
3838    public void setOnConversationListChangedListener(OnConversationUpdate listener) {
3839        final boolean remainingListeners;
3840        synchronized (LISTENER_LOCK) {
3841            remainingListeners = checkListeners();
3842            if (!this.mOnConversationUpdates.add(listener)) {
3843                Log.w(
3844                        Config.LOGTAG,
3845                        listener.getClass().getName()
3846                                + " is already registered as ConversationListChangedListener");
3847            }
3848            this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
3849        }
3850        if (remainingListeners) {
3851            switchToForeground();
3852        }
3853    }
3854
3855    public void removeOnConversationListChangedListener(OnConversationUpdate listener) {
3856        final boolean remainingListeners;
3857        synchronized (LISTENER_LOCK) {
3858            this.mOnConversationUpdates.remove(listener);
3859            this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
3860            remainingListeners = checkListeners();
3861        }
3862        if (remainingListeners) {
3863            switchToBackground();
3864        }
3865    }
3866
3867    public void setOnShowErrorToastListener(OnShowErrorToast listener) {
3868        final boolean remainingListeners;
3869        synchronized (LISTENER_LOCK) {
3870            remainingListeners = checkListeners();
3871            if (!this.mOnShowErrorToasts.add(listener)) {
3872                Log.w(
3873                        Config.LOGTAG,
3874                        listener.getClass().getName()
3875                                + " is already registered as OnShowErrorToastListener");
3876            }
3877        }
3878        if (remainingListeners) {
3879            switchToForeground();
3880        }
3881    }
3882
3883    public void removeOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
3884        final boolean remainingListeners;
3885        synchronized (LISTENER_LOCK) {
3886            this.mOnShowErrorToasts.remove(onShowErrorToast);
3887            remainingListeners = checkListeners();
3888        }
3889        if (remainingListeners) {
3890            switchToBackground();
3891        }
3892    }
3893
3894    public void setOnAccountListChangedListener(OnAccountUpdate listener) {
3895        final boolean remainingListeners;
3896        synchronized (LISTENER_LOCK) {
3897            remainingListeners = checkListeners();
3898            if (!this.mOnAccountUpdates.add(listener)) {
3899                Log.w(
3900                        Config.LOGTAG,
3901                        listener.getClass().getName()
3902                                + " is already registered as OnAccountListChangedtListener");
3903            }
3904        }
3905        if (remainingListeners) {
3906            switchToForeground();
3907        }
3908    }
3909
3910    public void removeOnAccountListChangedListener(OnAccountUpdate listener) {
3911        final boolean remainingListeners;
3912        synchronized (LISTENER_LOCK) {
3913            this.mOnAccountUpdates.remove(listener);
3914            remainingListeners = checkListeners();
3915        }
3916        if (remainingListeners) {
3917            switchToBackground();
3918        }
3919    }
3920
3921    public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
3922        final boolean remainingListeners;
3923        synchronized (LISTENER_LOCK) {
3924            remainingListeners = checkListeners();
3925            if (!this.mOnCaptchaRequested.add(listener)) {
3926                Log.w(
3927                        Config.LOGTAG,
3928                        listener.getClass().getName()
3929                                + " is already registered as OnCaptchaRequestListener");
3930            }
3931        }
3932        if (remainingListeners) {
3933            switchToForeground();
3934        }
3935    }
3936
3937    public void removeOnCaptchaRequestedListener(OnCaptchaRequested listener) {
3938        final boolean remainingListeners;
3939        synchronized (LISTENER_LOCK) {
3940            this.mOnCaptchaRequested.remove(listener);
3941            remainingListeners = checkListeners();
3942        }
3943        if (remainingListeners) {
3944            switchToBackground();
3945        }
3946    }
3947
3948    public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
3949        final boolean remainingListeners;
3950        synchronized (LISTENER_LOCK) {
3951            remainingListeners = checkListeners();
3952            if (!this.mOnRosterUpdates.add(listener)) {
3953                Log.w(
3954                        Config.LOGTAG,
3955                        listener.getClass().getName()
3956                                + " is already registered as OnRosterUpdateListener");
3957            }
3958        }
3959        if (remainingListeners) {
3960            switchToForeground();
3961        }
3962    }
3963
3964    public void removeOnRosterUpdateListener(final OnRosterUpdate listener) {
3965        final boolean remainingListeners;
3966        synchronized (LISTENER_LOCK) {
3967            this.mOnRosterUpdates.remove(listener);
3968            remainingListeners = checkListeners();
3969        }
3970        if (remainingListeners) {
3971            switchToBackground();
3972        }
3973    }
3974
3975    public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
3976        final boolean remainingListeners;
3977        synchronized (LISTENER_LOCK) {
3978            remainingListeners = checkListeners();
3979            if (!this.mOnUpdateBlocklist.add(listener)) {
3980                Log.w(
3981                        Config.LOGTAG,
3982                        listener.getClass().getName()
3983                                + " is already registered as OnUpdateBlocklistListener");
3984            }
3985        }
3986        if (remainingListeners) {
3987            switchToForeground();
3988        }
3989    }
3990
3991    public void removeOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
3992        final boolean remainingListeners;
3993        synchronized (LISTENER_LOCK) {
3994            this.mOnUpdateBlocklist.remove(listener);
3995            remainingListeners = checkListeners();
3996        }
3997        if (remainingListeners) {
3998            switchToBackground();
3999        }
4000    }
4001
4002    public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
4003        final boolean remainingListeners;
4004        synchronized (LISTENER_LOCK) {
4005            remainingListeners = checkListeners();
4006            if (!this.mOnKeyStatusUpdated.add(listener)) {
4007                Log.w(
4008                        Config.LOGTAG,
4009                        listener.getClass().getName()
4010                                + " is already registered as OnKeyStatusUpdateListener");
4011            }
4012        }
4013        if (remainingListeners) {
4014            switchToForeground();
4015        }
4016    }
4017
4018    public void removeOnNewKeysAvailableListener(final OnKeyStatusUpdated listener) {
4019        final boolean remainingListeners;
4020        synchronized (LISTENER_LOCK) {
4021            this.mOnKeyStatusUpdated.remove(listener);
4022            remainingListeners = checkListeners();
4023        }
4024        if (remainingListeners) {
4025            switchToBackground();
4026        }
4027    }
4028
4029    public void setOnRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
4030        final boolean remainingListeners;
4031        synchronized (LISTENER_LOCK) {
4032            remainingListeners = checkListeners();
4033            if (!this.onJingleRtpConnectionUpdate.add(listener)) {
4034                Log.w(
4035                        Config.LOGTAG,
4036                        listener.getClass().getName()
4037                                + " is already registered as OnJingleRtpConnectionUpdate");
4038            }
4039        }
4040        if (remainingListeners) {
4041            switchToForeground();
4042        }
4043    }
4044
4045    public void removeRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
4046        final boolean remainingListeners;
4047        synchronized (LISTENER_LOCK) {
4048            this.onJingleRtpConnectionUpdate.remove(listener);
4049            remainingListeners = checkListeners();
4050        }
4051        if (remainingListeners) {
4052            switchToBackground();
4053        }
4054    }
4055
4056    public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
4057        final boolean remainingListeners;
4058        synchronized (LISTENER_LOCK) {
4059            remainingListeners = checkListeners();
4060            if (!this.mOnMucRosterUpdate.add(listener)) {
4061                Log.w(
4062                        Config.LOGTAG,
4063                        listener.getClass().getName()
4064                                + " is already registered as OnMucRosterListener");
4065            }
4066        }
4067        if (remainingListeners) {
4068            switchToForeground();
4069        }
4070    }
4071
4072    public void removeOnMucRosterUpdateListener(final OnMucRosterUpdate listener) {
4073        final boolean remainingListeners;
4074        synchronized (LISTENER_LOCK) {
4075            this.mOnMucRosterUpdate.remove(listener);
4076            remainingListeners = checkListeners();
4077        }
4078        if (remainingListeners) {
4079            switchToBackground();
4080        }
4081    }
4082
4083    public boolean checkListeners() {
4084        return (this.mOnAccountUpdates.isEmpty()
4085                && this.mOnConversationUpdates.isEmpty()
4086                && this.mOnRosterUpdates.isEmpty()
4087                && this.mOnCaptchaRequested.isEmpty()
4088                && this.mOnMucRosterUpdate.isEmpty()
4089                && this.mOnUpdateBlocklist.isEmpty()
4090                && this.mOnShowErrorToasts.isEmpty()
4091                && this.onJingleRtpConnectionUpdate.isEmpty()
4092                && this.mOnKeyStatusUpdated.isEmpty());
4093    }
4094
4095    private void switchToForeground() {
4096        toggleSoftDisabled(false);
4097        final boolean broadcastLastActivity = broadcastLastActivity();
4098        for (Conversation conversation : getConversations()) {
4099            if (conversation.getMode() == Conversation.MODE_MULTI) {
4100                conversation.getMucOptions().resetChatState();
4101            } else {
4102                conversation.setIncomingChatState(Config.DEFAULT_CHAT_STATE);
4103            }
4104        }
4105        for (Account account : getAccounts()) {
4106            if (account.getStatus() == Account.State.ONLINE) {
4107                account.deactivateGracePeriod();
4108                final XmppConnection connection = account.getXmppConnection();
4109                if (connection != null) {
4110                    if (connection.getFeatures().csi()) {
4111                        connection.sendActive();
4112                    }
4113                    if (broadcastLastActivity) {
4114                        sendPresence(
4115                                account,
4116                                false); // send new presence but don't include idle because we are
4117                        // not
4118                    }
4119                }
4120            }
4121        }
4122        Log.d(Config.LOGTAG, "app switched into foreground");
4123    }
4124
4125    private void switchToBackground() {
4126        final boolean broadcastLastActivity = broadcastLastActivity();
4127        if (broadcastLastActivity) {
4128            mLastActivity = System.currentTimeMillis();
4129            final SharedPreferences.Editor editor = getPreferences().edit();
4130            editor.putLong(SETTING_LAST_ACTIVITY_TS, mLastActivity);
4131            editor.apply();
4132        }
4133        for (Account account : getAccounts()) {
4134            if (account.getStatus() == Account.State.ONLINE) {
4135                XmppConnection connection = account.getXmppConnection();
4136                if (connection != null) {
4137                    if (broadcastLastActivity) {
4138                        sendPresence(account, true);
4139                    }
4140                    if (connection.getFeatures().csi()) {
4141                        connection.sendInactive();
4142                    }
4143                }
4144            }
4145        }
4146        this.mNotificationService.setIsInForeground(false);
4147        Log.d(Config.LOGTAG, "app switched into background");
4148    }
4149
4150    public void connectMultiModeConversations(Account account) {
4151        List<Conversation> conversations = getConversations();
4152        for (Conversation conversation : conversations) {
4153            if (conversation.getMode() == Conversation.MODE_MULTI
4154                    && conversation.getAccount() == account) {
4155                joinMuc(conversation);
4156            }
4157        }
4158    }
4159
4160    public void mucSelfPingAndRejoin(final Conversation conversation) {
4161        final Account account = conversation.getAccount();
4162        synchronized (account.inProgressConferenceJoins) {
4163            if (account.inProgressConferenceJoins.contains(conversation)) {
4164                Log.d(
4165                        Config.LOGTAG,
4166                        account.getJid().asBareJid()
4167                                + ": canceling muc self ping because join is already under way");
4168                return;
4169            }
4170        }
4171        synchronized (account.inProgressConferencePings) {
4172            if (!account.inProgressConferencePings.add(conversation)) {
4173                Log.d(
4174                        Config.LOGTAG,
4175                        account.getJid().asBareJid()
4176                                + ": canceling muc self ping because ping is already under way");
4177                return;
4178            }
4179        }
4180        final Jid self = conversation.getMucOptions().getSelf().getFullJid();
4181        final Iq ping = new Iq(Iq.Type.GET);
4182        ping.setTo(self);
4183        ping.addChild("ping", Namespace.PING);
4184        sendIqPacket(
4185                conversation.getAccount(),
4186                ping,
4187                (response) -> {
4188                    if (response.getType() == Iq.Type.ERROR) {
4189                        final var error = response.getError();
4190                        if (error == null
4191                                || error.hasChild("service-unavailable")
4192                                || error.hasChild("feature-not-implemented")
4193                                || error.hasChild("item-not-found")) {
4194                            Log.d(
4195                                    Config.LOGTAG,
4196                                    account.getJid().asBareJid()
4197                                            + ": ping to "
4198                                            + self
4199                                            + " came back as ignorable error");
4200                        } else {
4201                            Log.d(
4202                                    Config.LOGTAG,
4203                                    account.getJid().asBareJid()
4204                                            + ": ping to "
4205                                            + self
4206                                            + " failed. attempting rejoin");
4207                            joinMuc(conversation);
4208                        }
4209                    } else if (response.getType() == Iq.Type.RESULT) {
4210                        Log.d(
4211                                Config.LOGTAG,
4212                                account.getJid().asBareJid()
4213                                        + ": ping to "
4214                                        + self
4215                                        + " came back fine");
4216                    }
4217                    synchronized (account.inProgressConferencePings) {
4218                        account.inProgressConferencePings.remove(conversation);
4219                    }
4220                });
4221    }
4222
4223    public void joinMuc(Conversation conversation) {
4224        joinMuc(conversation, null, false);
4225    }
4226
4227    public void joinMuc(Conversation conversation, boolean followedInvite) {
4228        joinMuc(conversation, null, followedInvite);
4229    }
4230
4231    private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
4232        joinMuc(conversation, onConferenceJoined, false);
4233    }
4234
4235    private void joinMuc(
4236            final Conversation conversation,
4237            final OnConferenceJoined onConferenceJoined,
4238            final boolean followedInvite) {
4239        final Account account = conversation.getAccount();
4240        synchronized (account.pendingConferenceJoins) {
4241            account.pendingConferenceJoins.remove(conversation);
4242        }
4243        synchronized (account.pendingConferenceLeaves) {
4244            account.pendingConferenceLeaves.remove(conversation);
4245        }
4246        if (account.getStatus() == Account.State.ONLINE) {
4247            synchronized (account.inProgressConferenceJoins) {
4248                account.inProgressConferenceJoins.add(conversation);
4249            }
4250            if (Config.MUC_LEAVE_BEFORE_JOIN) {
4251                sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions()));
4252            }
4253            conversation.resetMucOptions();
4254            if (onConferenceJoined != null) {
4255                conversation.getMucOptions().flagNoAutoPushConfiguration();
4256            }
4257            conversation.setHasMessagesLeftOnServer(false);
4258            fetchConferenceConfiguration(
4259                    conversation,
4260                    new OnConferenceConfigurationFetched() {
4261
4262                        private void join(Conversation conversation) {
4263                            Account account = conversation.getAccount();
4264                            final MucOptions mucOptions = conversation.getMucOptions();
4265
4266                            if (mucOptions.nonanonymous()
4267                                    && !mucOptions.membersOnly()
4268                                    && !conversation.getBooleanAttribute(
4269                                            "accept_non_anonymous", false)) {
4270                                synchronized (account.inProgressConferenceJoins) {
4271                                    account.inProgressConferenceJoins.remove(conversation);
4272                                }
4273                                mucOptions.setError(MucOptions.Error.NON_ANONYMOUS);
4274                                updateConversationUi();
4275                                if (onConferenceJoined != null) {
4276                                    onConferenceJoined.onConferenceJoined(conversation);
4277                                }
4278                                return;
4279                            }
4280
4281                            final Jid joinJid = mucOptions.getSelf().getFullJid();
4282                            Log.d(
4283                                    Config.LOGTAG,
4284                                    account.getJid().asBareJid().toString()
4285                                            + ": joining conversation "
4286                                            + joinJid.toString());
4287                            final var packet =
4288                                    mPresenceGenerator.selfPresence(
4289                                            account,
4290                                            Presence.Status.ONLINE,
4291                                            mucOptions.nonanonymous()
4292                                                    || onConferenceJoined != null,
4293                                            mucOptions.getSelf().getNick());
4294                            packet.setTo(joinJid);
4295                            Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
4296                            if (conversation.getMucOptions().getPassword() != null) {
4297                                x.addChild("password").setContent(mucOptions.getPassword());
4298                            }
4299
4300                            if (mucOptions.mamSupport()) {
4301                                // Use MAM instead of the limited muc history to get history
4302                                x.addChild("history").setAttribute("maxchars", "0");
4303                            } else {
4304                                // Fallback to muc history
4305                                x.addChild("history")
4306                                        .setAttribute(
4307                                                "since",
4308                                                PresenceGenerator.getTimestamp(
4309                                                        conversation
4310                                                                .getLastMessageTransmitted()
4311                                                                .getTimestamp()));
4312                            }
4313                            sendPresencePacket(account, packet);
4314                            if (onConferenceJoined != null) {
4315                                onConferenceJoined.onConferenceJoined(conversation);
4316                            }
4317                            if (!joinJid.equals(conversation.getJid())) {
4318                                conversation.setContactJid(joinJid);
4319                                databaseBackend.updateConversation(conversation);
4320                            }
4321
4322                            maybeRegisterWithMuc(conversation, null);
4323
4324                            if (mucOptions.mamSupport()) {
4325                                getMessageArchiveService().catchupMUC(conversation);
4326                            }
4327                            fetchConferenceMembers(conversation);
4328                            if (mucOptions.isPrivateAndNonAnonymous()) {
4329                                if (followedInvite) {
4330                                    final Bookmark bookmark = conversation.getBookmark();
4331                                    if (bookmark != null) {
4332                                        if (!bookmark.autojoin()) {
4333                                            bookmark.setAutojoin(true);
4334                                            createBookmark(account, bookmark);
4335                                        }
4336                                    } else {
4337                                        saveConversationAsBookmark(conversation, null);
4338                                    }
4339                                }
4340                            }
4341                            synchronized (account.inProgressConferenceJoins) {
4342                                account.inProgressConferenceJoins.remove(conversation);
4343                                sendUnsentMessages(conversation);
4344                            }
4345                        }
4346
4347                        @Override
4348                        public void onConferenceConfigurationFetched(Conversation conversation) {
4349                            if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
4350                                Log.d(
4351                                        Config.LOGTAG,
4352                                        account.getJid().asBareJid()
4353                                                + ": conversation ("
4354                                                + conversation.getJid()
4355                                                + ") got archived before IQ result");
4356                                return;
4357                            }
4358                            join(conversation);
4359                        }
4360
4361                        @Override
4362                        public void onFetchFailed(
4363                                final Conversation conversation, final String errorCondition) {
4364                            if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
4365                                Log.d(
4366                                        Config.LOGTAG,
4367                                        account.getJid().asBareJid()
4368                                                + ": conversation ("
4369                                                + conversation.getJid()
4370                                                + ") got archived before IQ result");
4371                                return;
4372                            }
4373                            if ("remote-server-not-found".equals(errorCondition)) {
4374                                synchronized (account.inProgressConferenceJoins) {
4375                                    account.inProgressConferenceJoins.remove(conversation);
4376                                }
4377                                conversation
4378                                        .getMucOptions()
4379                                        .setError(MucOptions.Error.SERVER_NOT_FOUND);
4380                                updateConversationUi();
4381                            } else {
4382                                join(conversation);
4383                                fetchConferenceConfiguration(conversation);
4384                            }
4385                        }
4386                    });
4387            updateConversationUi();
4388        } else {
4389            synchronized (account.pendingConferenceJoins) {
4390                account.pendingConferenceJoins.add(conversation);
4391            }
4392            conversation.resetMucOptions();
4393            conversation.setHasMessagesLeftOnServer(false);
4394            updateConversationUi();
4395        }
4396    }
4397
4398    private void fetchConferenceMembers(final Conversation conversation) {
4399        final Account account = conversation.getAccount();
4400        final AxolotlService axolotlService = account.getAxolotlService();
4401        final var affiliations = new ArrayList<String>();
4402        affiliations.add("outcast");
4403        if (conversation.getMucOptions().isPrivateAndNonAnonymous()) affiliations.addAll(List.of("member", "admin", "owner"));
4404        final Consumer<Iq> callback =
4405                new Consumer<Iq>() {
4406
4407                    private int i = 0;
4408                    private boolean success = true;
4409
4410                    @Override
4411                    public void accept(Iq response) {
4412                        final boolean omemoEnabled =
4413                                conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL;
4414                        Element query = response.query("http://jabber.org/protocol/muc#admin");
4415                        if (response.getType() == Iq.Type.RESULT && query != null) {
4416                            for (Element child : query.getChildren()) {
4417                                if ("item".equals(child.getName())) {
4418                                    MucOptions.User user =
4419                                            AbstractParser.parseItem(conversation, child);
4420                                    user.setOnline(false);
4421                                    if (!user.realJidMatchesAccount()) {
4422                                        boolean isNew =
4423                                                conversation.getMucOptions().updateUser(user);
4424                                        Contact contact = user.getContact();
4425                                        if (omemoEnabled
4426                                                && isNew
4427                                                && user.getRealJid() != null
4428                                                && (contact == null
4429                                                        || !contact.mutualPresenceSubscription())
4430                                                && axolotlService.hasEmptyDeviceList(
4431                                                        user.getRealJid())) {
4432                                            axolotlService.fetchDeviceIds(user.getRealJid());
4433                                        }
4434                                    }
4435                                }
4436                            }
4437                        } else {
4438                            success = false;
4439                            Log.d(
4440                                    Config.LOGTAG,
4441                                    account.getJid().asBareJid()
4442                                            + ": could not request affiliation "
4443                                            + affiliations.get(i)
4444                                            + " in "
4445                                            + conversation.getJid().asBareJid());
4446                        }
4447                        ++i;
4448                        if (i >= affiliations.size()) {
4449                            final var mucOptions = conversation.getMucOptions();
4450                            List<Jid> members = mucOptions.getMembers(true);
4451                            if (success) {
4452                                List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
4453                                boolean changed = false;
4454                                for (ListIterator<Jid> iterator = cryptoTargets.listIterator();
4455                                        iterator.hasNext(); ) {
4456                                    Jid jid = iterator.next();
4457                                    if (!members.contains(jid)
4458                                            && !members.contains(jid.getDomain())) {
4459                                        iterator.remove();
4460                                        Log.d(
4461                                                Config.LOGTAG,
4462                                                account.getJid().asBareJid()
4463                                                        + ": removed "
4464                                                        + jid
4465                                                        + " from crypto targets of "
4466                                                        + conversation.getName());
4467                                        changed = true;
4468                                    }
4469                                }
4470                                if (changed) {
4471                                    conversation.setAcceptedCryptoTargets(cryptoTargets);
4472                                    updateConversation(conversation);
4473                                }
4474                            }
4475                            getAvatarService().clear(mucOptions);
4476                            updateMucRosterUi();
4477                            updateConversationUi();
4478                        }
4479                    }
4480                };
4481        for (String affiliation : affiliations) {
4482            sendIqPacket(
4483                    account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
4484        }
4485        Log.d(
4486                Config.LOGTAG,
4487                account.getJid().asBareJid() + ": fetching members for " + conversation.getName());
4488    }
4489
4490    public void providePasswordForMuc(final Conversation conversation, final String password) {
4491        if (conversation.getMode() == Conversation.MODE_MULTI) {
4492            conversation.getMucOptions().setPassword(password);
4493            if (conversation.getBookmark() != null) {
4494                final Bookmark bookmark = conversation.getBookmark();
4495                bookmark.setAutojoin(true);
4496                createBookmark(conversation.getAccount(), bookmark);
4497            }
4498            updateConversation(conversation);
4499            joinMuc(conversation);
4500        }
4501    }
4502
4503    public void deleteAvatar(final Account account) {
4504        final AtomicBoolean executed = new AtomicBoolean(false);
4505        final Runnable onDeleted =
4506                () -> {
4507                    if (executed.compareAndSet(false, true)) {
4508                        account.setAvatar(null);
4509                        databaseBackend.updateAccount(account);
4510                        getAvatarService().clear(account);
4511                        updateAccountUi();
4512                    }
4513                };
4514        deleteVcardAvatar(account, onDeleted);
4515        deletePepNode(account, Namespace.AVATAR_DATA);
4516        deletePepNode(account, Namespace.AVATAR_METADATA, onDeleted);
4517    }
4518
4519    public void deletePepNode(final Account account, final String node) {
4520        deletePepNode(account, node, null);
4521    }
4522
4523    private void deletePepNode(final Account account, final String node, final Runnable runnable) {
4524        final Iq request = mIqGenerator.deleteNode(node);
4525        sendIqPacket(
4526                account,
4527                request,
4528                (packet) -> {
4529                    if (packet.getType() == Iq.Type.RESULT) {
4530                        Log.d(
4531                                Config.LOGTAG,
4532                                account.getJid().asBareJid()
4533                                        + ": successfully deleted pep node "
4534                                        + node);
4535                        if (runnable != null) {
4536                            runnable.run();
4537                        }
4538                    } else {
4539                        Log.d(
4540                                Config.LOGTAG,
4541                                account.getJid().asBareJid() + ": failed to delete " + packet);
4542                    }
4543                });
4544    }
4545
4546    private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) {
4547        final Iq retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid());
4548        sendIqPacket(
4549                account,
4550                retrieveVcard,
4551                (response) -> {
4552                    if (response.getType() != Iq.Type.RESULT) {
4553                        Log.d(
4554                                Config.LOGTAG,
4555                                account.getJid().asBareJid() + ": no vCard set. nothing to do");
4556                        return;
4557                    }
4558                    final Element vcard = response.findChild("vCard", "vcard-temp");
4559                    if (vcard == null) {
4560                        Log.d(
4561                                Config.LOGTAG,
4562                                account.getJid().asBareJid() + ": no vCard set. nothing to do");
4563                        return;
4564                    }
4565                    Element photo = vcard.findChild("PHOTO");
4566                    if (photo == null) {
4567                        photo = vcard.addChild("PHOTO");
4568                    }
4569                    photo.clearChildren();
4570                    final Iq publication = new Iq(Iq.Type.SET);
4571                    publication.setTo(account.getJid().asBareJid());
4572                    publication.addChild(vcard);
4573                    sendIqPacket(
4574                            account,
4575                            publication,
4576                            (publicationResponse) -> {
4577                                if (publicationResponse.getType() == Iq.Type.RESULT) {
4578                                    Log.d(
4579                                            Config.LOGTAG,
4580                                            account.getJid().asBareJid()
4581                                                    + ": successfully deleted vcard avatar");
4582                                    runnable.run();
4583                                } else {
4584                                    Log.d(
4585                                            Config.LOGTAG,
4586                                            "failed to publish vcard "
4587                                                    + publicationResponse.getErrorCondition());
4588                                }
4589                            });
4590                });
4591    }
4592
4593    private boolean hasEnabledAccounts() {
4594        if (this.accounts == null) {
4595            return false;
4596        }
4597        for (final Account account : this.accounts) {
4598            if (account.isConnectionEnabled()) {
4599                return true;
4600            }
4601        }
4602        return false;
4603    }
4604
4605    public void getAttachments(
4606            final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) {
4607        getAttachments(
4608                conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded);
4609    }
4610
4611    public void getAttachments(
4612            final Account account,
4613            final Jid jid,
4614            final int limit,
4615            final OnMediaLoaded onMediaLoaded) {
4616        getAttachments(account.getUuid(), jid.asBareJid(), limit, onMediaLoaded);
4617    }
4618
4619    public void getAttachments(
4620            final String account,
4621            final Jid jid,
4622            final int limit,
4623            final OnMediaLoaded onMediaLoaded) {
4624        new Thread(
4625                        () ->
4626                                onMediaLoaded.onMediaLoaded(
4627                                        fileBackend.convertToAttachments(
4628                                                databaseBackend.getRelativeFilePaths(
4629                                                        account, jid, limit))))
4630                .start();
4631    }
4632
4633    public void persistSelfNick(final MucOptions.User self, final boolean modified) {
4634        final Conversation conversation = self.getConversation();
4635        final Account account = conversation.getAccount();
4636        final Jid full = self.getFullJid();
4637        if (!full.equals(conversation.getJid())) {
4638            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persisting full jid " + full);
4639            conversation.setContactJid(full);
4640            databaseBackend.updateConversation(conversation);
4641        }
4642
4643        final String nick = self.getNick();
4644        final Bookmark bookmark = conversation.getBookmark();
4645        if (bookmark == null || !modified) {
4646            return;
4647        }
4648        final String defaultNick = MucOptions.defaultNick(account);
4649        if (nick.equals(defaultNick) || nick.equals(bookmark.getNick())) {
4650            return;
4651        }
4652        Log.d(
4653                Config.LOGTAG,
4654                account.getJid().asBareJid()
4655                        + ": persist nick '"
4656                        + full.getResource()
4657                        + "' into bookmark for "
4658                        + conversation.getJid().asBareJid());
4659        bookmark.setNick(nick);
4660        createBookmark(bookmark.getAccount(), bookmark);
4661    }
4662
4663    public void presenceToMuc(final Conversation conversation) {
4664        final MucOptions options = conversation.getMucOptions();
4665        if (options.online()) {
4666            Account account = conversation.getAccount();
4667            final Jid joinJid = options.getSelf().getFullJid();
4668            final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), options.getSelf().getNick());
4669            packet.setTo(joinJid);
4670            sendPresencePacket(account, packet);
4671        }
4672    }
4673
4674    public boolean renameInMuc(
4675            final Conversation conversation,
4676            final String nick,
4677            final UiCallback<Conversation> callback) {
4678        final Account account = conversation.getAccount();
4679        final Bookmark bookmark = conversation.getBookmark();
4680        final MucOptions options = conversation.getMucOptions();
4681        final Jid joinJid = options.createJoinJid(nick);
4682        if (joinJid == null) {
4683            return false;
4684        }
4685        if (options.online()) {
4686            maybeRegisterWithMuc(conversation, nick);
4687            options.setOnRenameListener(
4688                    new OnRenameListener() {
4689
4690                        @Override
4691                        public void onSuccess() {
4692                            final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick);
4693                            packet.setTo(joinJid);
4694                            sendPresencePacket(account, packet);
4695                            callback.success(conversation);
4696                        }
4697
4698                        @Override
4699                        public void onFailure() {
4700                            callback.error(R.string.nick_in_use, conversation);
4701                        }
4702                    });
4703
4704            final var packet =
4705                    mPresenceGenerator.selfPresence(
4706                            account, Presence.Status.ONLINE, options.nonanonymous(), nick);
4707            packet.setTo(joinJid);
4708            sendPresencePacket(account, packet);
4709            if (nick.equals(MucOptions.defaultNick(account))
4710                    && bookmark != null
4711                    && bookmark.getNick() != null) {
4712                Log.d(
4713                        Config.LOGTAG,
4714                        account.getJid().asBareJid()
4715                                + ": removing nick from bookmark for "
4716                                + bookmark.getJid());
4717                bookmark.setNick(null);
4718                createBookmark(account, bookmark);
4719            }
4720        } else {
4721            conversation.setContactJid(joinJid);
4722            databaseBackend.updateConversation(conversation);
4723            if (account.getStatus() == Account.State.ONLINE) {
4724                if (bookmark != null) {
4725                    bookmark.setNick(nick);
4726                    createBookmark(account, bookmark);
4727                }
4728                joinMuc(conversation);
4729            }
4730        }
4731        return true;
4732    }
4733
4734    public void checkMucRequiresRename() {
4735        synchronized (this.conversations) {
4736            for (final Conversation conversation : this.conversations) {
4737                if (conversation.getMode() == Conversational.MODE_MULTI) {
4738                    checkMucRequiresRename(conversation);
4739                }
4740            }
4741        }
4742    }
4743
4744    private void checkMucRequiresRename(final Conversation conversation) {
4745        final var options = conversation.getMucOptions();
4746        if (!options.online()) {
4747            return;
4748        }
4749        final var account = conversation.getAccount();
4750        final String current = options.getActualNick();
4751        final String proposed = options.getProposedNickPure();
4752        if (current == null || current.equals(proposed)) {
4753            return;
4754        }
4755        final Jid joinJid = options.createJoinJid(proposed);
4756        Log.d(
4757                Config.LOGTAG,
4758                String.format(
4759                        "%s: muc rename required %s (was: %s)",
4760                        account.getJid().asBareJid(), joinJid, current));
4761        final var packet =
4762                mPresenceGenerator.selfPresence(
4763                        account, Presence.Status.ONLINE, options.nonanonymous(), proposed);
4764        packet.setTo(joinJid);
4765        sendPresencePacket(account, packet);
4766    }
4767
4768    public void leaveMuc(Conversation conversation) {
4769        leaveMuc(conversation, false);
4770    }
4771
4772    private void leaveMuc(Conversation conversation, boolean now) {
4773        final Account account = conversation.getAccount();
4774        synchronized (account.pendingConferenceJoins) {
4775            account.pendingConferenceJoins.remove(conversation);
4776        }
4777        synchronized (account.pendingConferenceLeaves) {
4778            account.pendingConferenceLeaves.remove(conversation);
4779        }
4780        if (account.getStatus() == Account.State.ONLINE || now) {
4781            sendPresencePacket(
4782                    conversation.getAccount(),
4783                    mPresenceGenerator.leave(conversation.getMucOptions()));
4784            conversation.getMucOptions().setOffline();
4785            Bookmark bookmark = conversation.getBookmark();
4786            if (bookmark != null) {
4787                bookmark.setConversation(null);
4788            }
4789            Log.d(
4790                    Config.LOGTAG,
4791                    conversation.getAccount().getJid().asBareJid()
4792                            + ": leaving muc "
4793                            + conversation.getJid());
4794        } else {
4795            synchronized (account.pendingConferenceLeaves) {
4796                account.pendingConferenceLeaves.add(conversation);
4797            }
4798        }
4799    }
4800
4801    public String findConferenceServer(final Account account) {
4802        String server;
4803        if (account.getXmppConnection() != null) {
4804            server = account.getXmppConnection().getMucServer();
4805            if (server != null) {
4806                return server;
4807            }
4808        }
4809        for (Account other : getAccounts()) {
4810            if (other != account && other.getXmppConnection() != null) {
4811                server = other.getXmppConnection().getMucServer();
4812                if (server != null) {
4813                    return server;
4814                }
4815            }
4816        }
4817        return null;
4818    }
4819
4820    public void createPublicChannel(
4821            final Account account,
4822            final String name,
4823            final Jid address,
4824            final UiCallback<Conversation> callback) {
4825        joinMuc(
4826                findOrCreateConversation(account, address, true, false, true),
4827                conversation -> {
4828                    final Bundle configuration = IqGenerator.defaultChannelConfiguration();
4829                    if (!TextUtils.isEmpty(name)) {
4830                        configuration.putString("muc#roomconfig_roomname", name);
4831                    }
4832                    pushConferenceConfiguration(
4833                            conversation,
4834                            configuration,
4835                            new OnConfigurationPushed() {
4836                                @Override
4837                                public void onPushSucceeded() {
4838                                    saveConversationAsBookmark(conversation, name);
4839                                    callback.success(conversation);
4840                                }
4841
4842                                @Override
4843                                public void onPushFailed() {
4844                                    if (conversation
4845                                            .getMucOptions()
4846                                            .getSelf()
4847                                            .getAffiliation()
4848                                            .ranks(MucOptions.Affiliation.OWNER)) {
4849                                        callback.error(
4850                                                R.string.unable_to_set_channel_configuration,
4851                                                conversation);
4852                                    } else {
4853                                        callback.error(
4854                                                R.string.joined_an_existing_channel, conversation);
4855                                    }
4856                                }
4857                            });
4858                });
4859    }
4860
4861    public boolean createAdhocConference(
4862            final Account account,
4863            final String name,
4864            final Iterable<Jid> jids,
4865            final UiCallback<Conversation> callback) {
4866        Log.d(
4867                Config.LOGTAG,
4868                account.getJid().asBareJid().toString()
4869                        + ": creating adhoc conference with "
4870                        + jids.toString());
4871        if (account.getStatus() == Account.State.ONLINE) {
4872            try {
4873                String server = findConferenceServer(account);
4874                if (server == null) {
4875                    if (callback != null) {
4876                        callback.error(R.string.no_conference_server_found, null);
4877                    }
4878                    return false;
4879                }
4880                final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null);
4881                final Conversation conversation =
4882                        findOrCreateConversation(account, jid, true, false, true);
4883                joinMuc(
4884                        conversation,
4885                        new OnConferenceJoined() {
4886                            @Override
4887                            public void onConferenceJoined(final Conversation conversation) {
4888                                final Bundle configuration =
4889                                        IqGenerator.defaultGroupChatConfiguration();
4890                                if (!TextUtils.isEmpty(name)) {
4891                                    configuration.putString("muc#roomconfig_roomname", name);
4892                                }
4893                                pushConferenceConfiguration(
4894                                        conversation,
4895                                        configuration,
4896                                        new OnConfigurationPushed() {
4897                                            @Override
4898                                            public void onPushSucceeded() {
4899                                                for (Jid invite : jids) {
4900                                                    invite(conversation, invite);
4901                                                }
4902                                                for (String resource :
4903                                                        account.getSelfContact()
4904                                                                .getPresences()
4905                                                                .toResourceArray()) {
4906                                                    Jid other =
4907                                                            account.getJid().withResource(resource);
4908                                                    Log.d(
4909                                                            Config.LOGTAG,
4910                                                            account.getJid().asBareJid()
4911                                                                    + ": sending direct invite to "
4912                                                                    + other);
4913                                                    directInvite(conversation, other);
4914                                                }
4915                                                saveConversationAsBookmark(conversation, name);
4916                                                if (callback != null) {
4917                                                    callback.success(conversation);
4918                                                }
4919                                            }
4920
4921                                            @Override
4922                                            public void onPushFailed() {
4923                                                archiveConversation(conversation);
4924                                                if (callback != null) {
4925                                                    callback.error(
4926                                                            R.string.conference_creation_failed,
4927                                                            conversation);
4928                                                }
4929                                            }
4930                                        });
4931                            }
4932                        });
4933                return true;
4934            } catch (IllegalArgumentException e) {
4935                if (callback != null) {
4936                    callback.error(R.string.conference_creation_failed, null);
4937                }
4938                return false;
4939            }
4940        } else {
4941            if (callback != null) {
4942                callback.error(R.string.not_connected_try_again, null);
4943            }
4944            return false;
4945        }
4946    }
4947
4948    public void checkIfMuc(final Account account, final Jid jid, Consumer<Boolean> cb) {
4949        if (jid.isDomainJid()) {
4950            // Spec basically says MUC needs to have a node
4951            // And also specifies that MUC and MUC service should have the same identity...
4952            cb.accept(false);
4953            return;
4954        }
4955
4956        final var request = mIqGenerator.queryDiscoInfo(jid.asBareJid());
4957        sendIqPacket(account, request, (reply) -> {
4958            final var result = new ServiceDiscoveryResult(reply);
4959            cb.accept(
4960                result.getFeatures().contains("http://jabber.org/protocol/muc") &&
4961                result.hasIdentity("conference", null)
4962            );
4963        });
4964    }
4965
4966    public void fetchConferenceConfiguration(final Conversation conversation) {
4967        fetchConferenceConfiguration(conversation, null);
4968    }
4969
4970    public void fetchConferenceConfiguration(
4971            final Conversation conversation, final OnConferenceConfigurationFetched callback) {
4972        final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid());
4973        final var account = conversation.getAccount();
4974        sendIqPacket(
4975                account,
4976                request,
4977                response -> {
4978                    if (response.getType() == Iq.Type.RESULT) {
4979                        final MucOptions mucOptions = conversation.getMucOptions();
4980                        final Bookmark bookmark = conversation.getBookmark();
4981                        final boolean sameBefore =
4982                                StringUtils.equals(
4983                                        bookmark == null ? null : bookmark.getBookmarkName(),
4984                                        mucOptions.getName());
4985
4986                        final var hadOccupantId = mucOptions.occupantId();
4987                        if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(response))) {
4988                            Log.d(
4989                                    Config.LOGTAG,
4990                                    account.getJid().asBareJid()
4991                                            + ": muc configuration changed for "
4992                                            + conversation.getJid().asBareJid());
4993                            updateConversation(conversation);
4994                        }
4995
4996                        final var hasOccupantId = mucOptions.occupantId();
4997
4998                        if (!hadOccupantId && hasOccupantId && mucOptions.online()) {
4999                            final var me = mucOptions.getSelf().getFullJid();
5000                            Log.d(
5001                                    Config.LOGTAG,
5002                                    account.getJid().asBareJid()
5003                                            + ": gained support for occupant-id in "
5004                                            + me
5005                                            + ". resending presence");
5006                            final var packet =
5007                                    mPresenceGenerator.selfPresence(
5008                                            account,
5009                                            Presence.Status.ONLINE,
5010                                            mucOptions.nonanonymous(), mucOptions.getSelf().getNick());
5011                            packet.setTo(me);
5012                            sendPresencePacket(account, packet);
5013                        }
5014
5015                        if (bookmark != null
5016                                && (sameBefore || bookmark.getBookmarkName() == null)) {
5017                            if (bookmark.setBookmarkName(
5018                                    StringUtils.nullOnEmpty(mucOptions.getName()))) {
5019                                createBookmark(account, bookmark);
5020                            }
5021                        }
5022
5023                        if (callback != null) {
5024                            callback.onConferenceConfigurationFetched(conversation);
5025                        }
5026
5027                        updateConversationUi();
5028                    } else if (response.getType() == Iq.Type.TIMEOUT) {
5029                        Log.d(
5030                                Config.LOGTAG,
5031                                account.getJid().asBareJid()
5032                                        + ": received timeout waiting for conference configuration"
5033                                        + " fetch");
5034                    } else {
5035                        if (callback != null) {
5036                            callback.onFetchFailed(conversation, response.getErrorCondition());
5037                        }
5038                    }
5039                });
5040    }
5041
5042    public void pushNodeConfiguration(
5043            Account account,
5044            final String node,
5045            final Bundle options,
5046            final OnConfigurationPushed callback) {
5047        pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback);
5048    }
5049
5050    public void pushNodeConfiguration(
5051            Account account,
5052            final Jid jid,
5053            final String node,
5054            final Bundle options,
5055            final OnConfigurationPushed callback) {
5056        Log.d(Config.LOGTAG, "pushing node configuration");
5057        sendIqPacket(
5058                account,
5059                mIqGenerator.requestPubsubConfiguration(jid, node),
5060                responseToRequest -> {
5061                    if (responseToRequest.getType() == Iq.Type.RESULT) {
5062                        Element pubsub =
5063                                responseToRequest.findChild(
5064                                        "pubsub", "http://jabber.org/protocol/pubsub#owner");
5065                        Element configuration =
5066                                pubsub == null ? null : pubsub.findChild("configure");
5067                        Element x =
5068                                configuration == null
5069                                        ? null
5070                                        : configuration.findChild("x", Namespace.DATA);
5071                        if (x != null) {
5072                            final Data data = Data.parse(x);
5073                            data.submit(options);
5074                            sendIqPacket(
5075                                    account,
5076                                    mIqGenerator.publishPubsubConfiguration(jid, node, data),
5077                                    responseToPublish -> {
5078                                        if (responseToPublish.getType() == Iq.Type.RESULT
5079                                                && callback != null) {
5080                                            Log.d(
5081                                                    Config.LOGTAG,
5082                                                    account.getJid().asBareJid()
5083                                                            + ": successfully changed node"
5084                                                            + " configuration for node "
5085                                                            + node);
5086                                            callback.onPushSucceeded();
5087                                        } else if (responseToPublish.getType() == Iq.Type.ERROR
5088                                                && callback != null) {
5089                                            callback.onPushFailed();
5090                                        }
5091                                    });
5092                        } else if (callback != null) {
5093                            callback.onPushFailed();
5094                        }
5095                    } else if (responseToRequest.getType() == Iq.Type.ERROR && callback != null) {
5096                        callback.onPushFailed();
5097                    }
5098                });
5099    }
5100
5101    public void pushConferenceConfiguration(
5102            final Conversation conversation,
5103            final Bundle options,
5104            final OnConfigurationPushed callback) {
5105        if (options.getString("muc#roomconfig_whois", "moderators").equals("anyone")) {
5106            conversation.setAttribute("accept_non_anonymous", true);
5107            updateConversation(conversation);
5108        }
5109        if (options.containsKey("muc#roomconfig_moderatedroom")) {
5110            final boolean moderated = "1".equals(options.getString("muc#roomconfig_moderatedroom"));
5111            options.putString("members_by_default", moderated ? "0" : "1");
5112        }
5113        if (options.containsKey("muc#roomconfig_allowpm")) {
5114            // ejabberd :-/
5115            final boolean allow = "anyone".equals(options.getString("muc#roomconfig_allowpm"));
5116            options.putString("allow_private_messages", allow ? "1" : "0");
5117            options.putString("allow_private_messages_from_visitors", allow ? "anyone" : "nobody");
5118        }
5119        final var account = conversation.getAccount();
5120        final Iq request = new Iq(Iq.Type.GET);
5121        request.setTo(conversation.getJid().asBareJid());
5122        request.query("http://jabber.org/protocol/muc#owner");
5123        sendIqPacket(
5124                account,
5125                request,
5126                response -> {
5127                    if (response.getType() == Iq.Type.RESULT) {
5128                        final Data data =
5129                                Data.parse(response.query().findChild("x", Namespace.DATA));
5130                        data.submit(options);
5131                        final Iq set = new Iq(Iq.Type.SET);
5132                        set.setTo(conversation.getJid().asBareJid());
5133                        set.query("http://jabber.org/protocol/muc#owner").addChild(data);
5134                        sendIqPacket(
5135                                account,
5136                                set,
5137                                packet -> {
5138                                    if (callback != null) {
5139                                        if (packet.getType() == Iq.Type.RESULT) {
5140                                            callback.onPushSucceeded();
5141                                        } else {
5142                                            Log.d(Config.LOGTAG, "failed: " + packet.toString());
5143                                            callback.onPushFailed();
5144                                        }
5145                                    }
5146                                });
5147                    } else {
5148                        if (callback != null) {
5149                            callback.onPushFailed();
5150                        }
5151                    }
5152                });
5153    }
5154
5155    public void pushSubjectToConference(final Conversation conference, final String subject) {
5156        final var packet =
5157                this.getMessageGenerator()
5158                        .conferenceSubject(conference, StringUtils.nullOnEmpty(subject));
5159        this.sendMessagePacket(conference.getAccount(), packet);
5160    }
5161
5162    public void requestVoice(final Account account, final Jid jid) {
5163        final var packet = this.getMessageGenerator().requestVoice(jid);
5164        this.sendMessagePacket(account, packet);
5165    }
5166
5167    public void changeAffiliationInConference(
5168            final Conversation conference,
5169            Jid user,
5170            final MucOptions.Affiliation affiliation,
5171            final OnAffiliationChanged callback) {
5172        final Jid jid = user.asBareJid();
5173        final Iq request =
5174                this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
5175        sendIqPacket(
5176                conference.getAccount(),
5177                request,
5178                (response) -> {
5179                    if (response.getType() == Iq.Type.RESULT) {
5180                        final var mucOptions = conference.getMucOptions();
5181                        mucOptions.changeAffiliation(jid, affiliation);
5182                        getAvatarService().clear(mucOptions);
5183                        if (callback != null) {
5184                            callback.onAffiliationChangedSuccessful(jid);
5185                        } else {
5186                            Log.d(
5187                                    Config.LOGTAG,
5188                                    "changed affiliation of " + user + " to " + affiliation);
5189                        }
5190                    } else if (callback != null) {
5191                        callback.onAffiliationChangeFailed(
5192                                jid, R.string.could_not_change_affiliation);
5193                    } else {
5194                        Log.d(Config.LOGTAG, "unable to change affiliation");
5195                    }
5196                });
5197    }
5198
5199    public void changeRoleInConference(
5200            final Conversation conference, final String nick, MucOptions.Role role) {
5201        final var account = conference.getAccount();
5202        final Iq request = this.mIqGenerator.changeRole(conference, nick, role.toString());
5203        sendIqPacket(
5204                account,
5205                request,
5206                (packet) -> {
5207                    if (packet.getType() != Iq.Type.RESULT) {
5208                        Log.d(
5209                                Config.LOGTAG,
5210                                account.getJid().asBareJid() + " unable to change role of " + nick);
5211                    }
5212                });
5213    }
5214
5215    public void moderateMessage(final Account account, final Message m, final String reason) {
5216        final var request = this.mIqGenerator.moderateMessage(account, m, reason);
5217        sendIqPacket(account, request, (packet) -> {
5218            if (packet.getType() != Iq.Type.RESULT) {
5219                showErrorToastInUi(R.string.unable_to_moderate);
5220                Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to moderate: " + packet);
5221            }
5222        });
5223    }
5224
5225    public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) {
5226        final Iq request = new Iq(Iq.Type.SET);
5227        request.setTo(conversation.getJid().asBareJid());
5228        request.query("http://jabber.org/protocol/muc#owner").addChild("destroy");
5229        sendIqPacket(
5230                conversation.getAccount(),
5231                request,
5232                response -> {
5233                    if (response.getType() == Iq.Type.RESULT) {
5234                        if (callback != null) {
5235                            callback.onRoomDestroySucceeded();
5236                        }
5237                    } else if (response.getType() == Iq.Type.ERROR) {
5238                        if (callback != null) {
5239                            callback.onRoomDestroyFailed();
5240                        }
5241                    }
5242                });
5243    }
5244
5245    private void disconnect(final Account account, boolean force) {
5246        final XmppConnection connection = account.getXmppConnection();
5247        if (connection == null) {
5248            return;
5249        }
5250        if (!force) {
5251            final List<Conversation> conversations = getConversations();
5252            for (Conversation conversation : conversations) {
5253                if (conversation.getAccount() == account) {
5254                    if (conversation.getMode() == Conversation.MODE_MULTI) {
5255                        leaveMuc(conversation, true);
5256                    }
5257                }
5258            }
5259            sendOfflinePresence(account);
5260        }
5261        connection.disconnect(force);
5262    }
5263
5264    @Override
5265    public IBinder onBind(Intent intent) {
5266        return mBinder;
5267    }
5268
5269    public void deleteMessage(Message message) {
5270        mScheduledMessages.remove(message.getUuid());
5271        databaseBackend.deleteMessage(message.getUuid());
5272        ((Conversation) message.getConversation()).remove(message);
5273        updateConversationUi();
5274    }
5275
5276    public void updateMessage(Message message) {
5277        updateMessage(message, true);
5278    }
5279
5280    public void updateMessage(Message message, boolean includeBody) {
5281        databaseBackend.updateMessage(message, includeBody);
5282        updateConversationUi();
5283    }
5284
5285    public void createMessageAsync(final Message message) {
5286        mDatabaseWriterExecutor.execute(() -> databaseBackend.createMessage(message));
5287    }
5288
5289    public void updateMessage(Message message, String uuid) {
5290        if (!databaseBackend.updateMessage(message, uuid)) {
5291            Log.e(Config.LOGTAG, "error updated message in DB after edit");
5292        }
5293        updateConversationUi();
5294    }
5295
5296    public void syncDirtyContacts(Account account) {
5297        for (Contact contact : account.getRoster().getContacts()) {
5298            if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
5299                pushContactToServer(contact);
5300            }
5301            if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
5302                deleteContactOnServer(contact);
5303            }
5304        }
5305    }
5306
5307    protected void unregisterPhoneAccounts(final Account account) {
5308        for (final Contact contact : account.getRoster().getContacts()) {
5309            if (!contact.showInRoster()) {
5310                contact.unregisterAsPhoneAccount(this);
5311            }
5312        }
5313    }
5314
5315    public void createContact(final Contact contact, final boolean autoGrant) {
5316        createContact(contact, autoGrant, null);
5317    }
5318
5319    public void createContact(
5320            final Contact contact, final boolean autoGrant, final String preAuth) {
5321        if (autoGrant) {
5322            contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
5323            contact.setOption(Contact.Options.ASKING);
5324        }
5325        pushContactToServer(contact, preAuth);
5326    }
5327
5328    public void pushContactToServer(final Contact contact) {
5329        pushContactToServer(contact, null);
5330    }
5331
5332    private void pushContactToServer(final Contact contact, final String preAuth) {
5333        contact.resetOption(Contact.Options.DIRTY_DELETE);
5334        contact.setOption(Contact.Options.DIRTY_PUSH);
5335        final Account account = contact.getAccount();
5336        if (account.getStatus() == Account.State.ONLINE) {
5337            final boolean ask = contact.getOption(Contact.Options.ASKING);
5338            final boolean sendUpdates =
5339                    contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
5340                            && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
5341            final Iq iq = new Iq(Iq.Type.SET);
5342            iq.query(Namespace.ROSTER).addChild(contact.asElement());
5343            account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
5344            if (sendUpdates) {
5345                sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
5346            }
5347            if (ask) {
5348                sendPresencePacket(
5349                        account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth));
5350            }
5351        } else {
5352            syncRoster(contact.getAccount());
5353        }
5354    }
5355
5356    public void publishMucAvatar(
5357            final Conversation conversation, final Uri image, final OnAvatarPublication callback) {
5358        new Thread(
5359                        () -> {
5360                            final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
5361                            final int size = Config.AVATAR_SIZE;
5362                            final Avatar avatar =
5363                                    getFileBackend().getPepAvatar(image, size, format);
5364                            if (avatar != null) {
5365                                if (!getFileBackend().save(avatar)) {
5366                                    callback.onAvatarPublicationFailed(
5367                                            R.string.error_saving_avatar);
5368                                    return;
5369                                }
5370                                avatar.owner = conversation.getJid().asBareJid();
5371                                publishMucAvatar(conversation, avatar, callback);
5372                            } else {
5373                                callback.onAvatarPublicationFailed(
5374                                        R.string.error_publish_avatar_converting);
5375                            }
5376                        })
5377                .start();
5378    }
5379
5380    public void publishAvatarAsync(
5381            final Account account,
5382            final Uri image,
5383            final boolean open,
5384            final OnAvatarPublication callback) {
5385        new Thread(() -> publishAvatar(account, image, open, callback)).start();
5386    }
5387
5388    private void publishAvatar(
5389            final Account account,
5390            final Uri image,
5391            final boolean open,
5392            final OnAvatarPublication callback) {
5393        final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
5394        final int size = Config.AVATAR_SIZE;
5395        final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
5396        if (avatar != null) {
5397            if (!getFileBackend().save(avatar)) {
5398                Log.d(Config.LOGTAG, "unable to save vcard");
5399                callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
5400                return;
5401            }
5402            publishAvatar(account, avatar, open, callback);
5403        } else {
5404            callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
5405        }
5406    }
5407
5408    private void publishMucAvatar(
5409            Conversation conversation, Avatar avatar, OnAvatarPublication callback) {
5410        final var account = conversation.getAccount();
5411        final Iq retrieve = mIqGenerator.retrieveVcardAvatar(avatar);
5412        sendIqPacket(
5413                account,
5414                retrieve,
5415                (response) -> {
5416                    boolean itemNotFound =
5417                            response.getType() == Iq.Type.ERROR
5418                                    && response.hasChild("error")
5419                                    && response.findChild("error").hasChild("item-not-found");
5420                    if (response.getType() == Iq.Type.RESULT || itemNotFound) {
5421                        Element vcard = response.findChild("vCard", "vcard-temp");
5422                        if (vcard == null) {
5423                            vcard = new Element("vCard", "vcard-temp");
5424                        }
5425                        Element photo = vcard.findChild("PHOTO");
5426                        if (photo == null) {
5427                            photo = vcard.addChild("PHOTO");
5428                        }
5429                        photo.clearChildren();
5430                        photo.addChild("TYPE").setContent(avatar.type);
5431                        photo.addChild("BINVAL").setContent(avatar.image);
5432                        final Iq publication = new Iq(Iq.Type.SET);
5433                        publication.setTo(conversation.getJid().asBareJid());
5434                        publication.addChild(vcard);
5435                        sendIqPacket(
5436                                account,
5437                                publication,
5438                                (publicationResponse) -> {
5439                                    if (publicationResponse.getType() == Iq.Type.RESULT) {
5440                                        callback.onAvatarPublicationSucceeded();
5441                                    } else {
5442                                        Log.d(
5443                                                Config.LOGTAG,
5444                                                "failed to publish vcard "
5445                                                        + publicationResponse.getErrorCondition());
5446                                        callback.onAvatarPublicationFailed(
5447                                                R.string.error_publish_avatar_server_reject);
5448                                    }
5449                                });
5450                    } else {
5451                        Log.d(Config.LOGTAG, "failed to request vcard " + response);
5452                        callback.onAvatarPublicationFailed(
5453                                R.string.error_publish_avatar_no_server_support);
5454                    }
5455                });
5456    }
5457
5458    public void publishAvatar(
5459            final Account account,
5460            final Avatar avatar,
5461            final boolean open,
5462            final OnAvatarPublication callback) {
5463        final Bundle options;
5464        if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
5465            options = open ? PublishOptions.openAccess() : PublishOptions.presenceAccess();
5466        } else {
5467            options = null;
5468        }
5469        publishAvatar(account, avatar, options, true, callback);
5470    }
5471
5472    public void publishAvatar(
5473            Account account,
5474            final Avatar avatar,
5475            final Bundle options,
5476            final boolean retry,
5477            final OnAvatarPublication callback) {
5478        Log.d(
5479                Config.LOGTAG,
5480                account.getJid().asBareJid() + ": publishing avatar. options=" + options);
5481        final Iq packet = this.mIqGenerator.publishAvatar(avatar, options);
5482        this.sendIqPacket(
5483                account,
5484                packet,
5485                result -> {
5486                    if (result.getType() == Iq.Type.RESULT) {
5487                        publishAvatarMetadata(account, avatar, options, true, callback);
5488                    } else if (retry && PublishOptions.preconditionNotMet(result)) {
5489                        pushNodeConfiguration(
5490                                account,
5491                                Namespace.AVATAR_DATA,
5492                                options,
5493                                new OnConfigurationPushed() {
5494                                    @Override
5495                                    public void onPushSucceeded() {
5496                                        Log.d(
5497                                                Config.LOGTAG,
5498                                                account.getJid().asBareJid()
5499                                                        + ": changed node configuration for avatar"
5500                                                        + " node");
5501                                        publishAvatar(account, avatar, options, false, callback);
5502                                    }
5503
5504                                    @Override
5505                                    public void onPushFailed() {
5506                                        Log.d(
5507                                                Config.LOGTAG,
5508                                                account.getJid().asBareJid()
5509                                                        + ": unable to change node configuration"
5510                                                        + " for avatar node");
5511                                        publishAvatar(account, avatar, null, false, callback);
5512                                    }
5513                                });
5514                    } else {
5515                        Element error = result.findChild("error");
5516                        Log.d(
5517                                Config.LOGTAG,
5518                                account.getJid().asBareJid()
5519                                        + ": server rejected avatar "
5520                                        + (avatar.size / 1024)
5521                                        + "KiB "
5522                                        + (error != null ? error.toString() : ""));
5523                        if (callback != null) {
5524                            callback.onAvatarPublicationFailed(
5525                                    R.string.error_publish_avatar_server_reject);
5526                        }
5527                    }
5528                });
5529    }
5530
5531    public void publishAvatarMetadata(
5532            Account account,
5533            final Avatar avatar,
5534            final Bundle options,
5535            final boolean retry,
5536            final OnAvatarPublication callback) {
5537        final Iq packet =
5538                XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options);
5539        sendIqPacket(
5540                account,
5541                packet,
5542                result -> {
5543                    if (result.getType() == Iq.Type.RESULT) {
5544                        if (account.setAvatar(avatar.getFilename())) {
5545                            getAvatarService().clear(account);
5546                            databaseBackend.updateAccount(account);
5547                            notifyAccountAvatarHasChanged(account);
5548                        }
5549                        Log.d(
5550                                Config.LOGTAG,
5551                                account.getJid().asBareJid()
5552                                        + ": published avatar "
5553                                        + (avatar.size / 1024)
5554                                        + "KiB");
5555                        if (callback != null) {
5556                            callback.onAvatarPublicationSucceeded();
5557                        }
5558                    } else if (retry && PublishOptions.preconditionNotMet(result)) {
5559                        pushNodeConfiguration(
5560                                account,
5561                                Namespace.AVATAR_METADATA,
5562                                options,
5563                                new OnConfigurationPushed() {
5564                                    @Override
5565                                    public void onPushSucceeded() {
5566                                        Log.d(
5567                                                Config.LOGTAG,
5568                                                account.getJid().asBareJid()
5569                                                        + ": changed node configuration for avatar"
5570                                                        + " meta data node");
5571                                        publishAvatarMetadata(
5572                                                account, avatar, options, false, callback);
5573                                    }
5574
5575                                    @Override
5576                                    public void onPushFailed() {
5577                                        Log.d(
5578                                                Config.LOGTAG,
5579                                                account.getJid().asBareJid()
5580                                                        + ": unable to change node configuration"
5581                                                        + " for avatar meta data node");
5582                                        publishAvatarMetadata(
5583                                                account, avatar, null, false, callback);
5584                                    }
5585                                });
5586                    } else {
5587                        if (callback != null) {
5588                            callback.onAvatarPublicationFailed(
5589                                    R.string.error_publish_avatar_server_reject);
5590                        }
5591                    }
5592                });
5593    }
5594
5595    public void republishAvatarIfNeeded(final Account account) {
5596        if (account.getAxolotlService().isPepBroken()) {
5597            Log.d(
5598                    Config.LOGTAG,
5599                    account.getJid().asBareJid()
5600                            + ": skipping republication of avatar because pep is broken");
5601            return;
5602        }
5603        final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
5604        this.sendIqPacket(
5605                account,
5606                packet,
5607                new Consumer<Iq>() {
5608
5609                    private Avatar parseAvatar(final Iq packet) {
5610                        final var pubsub = packet.getExtension(PubSub.class);
5611                        if (pubsub == null) {
5612                            return null;
5613                        }
5614                        final var items = pubsub.getItems();
5615                        if (items == null) {
5616                            return null;
5617                        }
5618                        final var item = items.getFirstItemWithId(Metadata.class);
5619                        if (item == null) {
5620                            return null;
5621                        }
5622                        return Avatar.parseMetadata(item.getKey(), item.getValue());
5623                    }
5624
5625                    private boolean errorIsItemNotFound(Iq packet) {
5626                        Element error = packet.findChild("error");
5627                        return packet.getType() == Iq.Type.ERROR
5628                                && error != null
5629                                && error.hasChild("item-not-found");
5630                    }
5631
5632                    @Override
5633                    public void accept(final Iq packet) {
5634                        if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) {
5635                            final Avatar serverAvatar = parseAvatar(packet);
5636                            if (serverAvatar == null && account.getAvatar() != null) {
5637                                final Avatar avatar =
5638                                        fileBackend.getStoredPepAvatar(account.getAvatar());
5639                                if (avatar != null) {
5640                                    Log.d(
5641                                            Config.LOGTAG,
5642                                            account.getJid().asBareJid()
5643                                                    + ": avatar on server was null. republishing");
5644                                    // publishing as 'open' - old server (that requires
5645                                    // republication) likely doesn't support access models anyway
5646                                    publishAvatar(
5647                                            account,
5648                                            fileBackend.getStoredPepAvatar(account.getAvatar()),
5649                                            true,
5650                                            null);
5651                                } else {
5652                                    Log.e(
5653                                            Config.LOGTAG,
5654                                            account.getJid().asBareJid()
5655                                                    + ": error rereading avatar");
5656                                }
5657                            }
5658                        }
5659                    }
5660                });
5661    }
5662
5663    public void cancelAvatarFetches(final Account account) {
5664        synchronized (mInProgressAvatarFetches) {
5665            for (final Iterator<String> iterator = mInProgressAvatarFetches.iterator();
5666                    iterator.hasNext(); ) {
5667                final String KEY = iterator.next();
5668                if (KEY.startsWith(account.getJid().asBareJid() + "_")) {
5669                    iterator.remove();
5670                }
5671            }
5672        }
5673    }
5674
5675    public void fetchAvatar(Account account, Avatar avatar) {
5676        fetchAvatar(account, avatar, null);
5677    }
5678
5679    public void fetchAvatar(
5680            Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5681        if (databaseBackend.isBlockedMedia(avatar.cid())) {
5682            if (callback != null) callback.error(0, null);
5683            return;
5684        }
5685
5686        final String KEY = generateFetchKey(account, avatar);
5687        synchronized (this.mInProgressAvatarFetches) {
5688            if (mInProgressAvatarFetches.add(KEY)) {
5689                switch (avatar.origin) {
5690                    case PEP:
5691                        this.mInProgressAvatarFetches.add(KEY);
5692                        fetchAvatarPep(account, avatar, callback);
5693                        break;
5694                    case VCARD:
5695                        this.mInProgressAvatarFetches.add(KEY);
5696                        fetchAvatarVcard(account, avatar, callback);
5697                        break;
5698                }
5699            } else if (avatar.origin == Avatar.Origin.PEP) {
5700                mOmittedPepAvatarFetches.add(KEY);
5701            } else {
5702                Log.d(
5703                        Config.LOGTAG,
5704                        account.getJid().asBareJid()
5705                                + ": already fetching "
5706                                + avatar.origin
5707                                + " avatar for "
5708                                + avatar.owner);
5709            }
5710        }
5711    }
5712
5713    private void fetchAvatarPep(
5714            final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5715        final Iq packet = this.mIqGenerator.retrievePepAvatar(avatar);
5716        sendIqPacket(
5717                account,
5718                packet,
5719                (result) -> {
5720                    synchronized (mInProgressAvatarFetches) {
5721                        mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
5722                    }
5723                    final String ERROR =
5724                            account.getJid().asBareJid()
5725                                    + ": fetching avatar for "
5726                                    + avatar.owner
5727                                    + " failed ";
5728                    if (result.getType() == Iq.Type.RESULT) {
5729                        avatar.image = IqParser.avatarData(result);
5730                        if (avatar.image != null) {
5731                            if (getFileBackend().save(avatar)) {
5732                                if (account.getJid().asBareJid().equals(avatar.owner)) {
5733                                    if (account.setAvatar(avatar.getFilename())) {
5734                                        databaseBackend.updateAccount(account);
5735                                    }
5736                                    getAvatarService().clear(account);
5737                                    updateConversationUi();
5738                                    updateAccountUi();
5739                                } else {
5740                                    final Contact contact =
5741                                            account.getRoster().getContact(avatar.owner);
5742                                    contact.setAvatar(avatar);
5743                                    syncRoster(account);
5744                                    getAvatarService().clear(contact);
5745                                    updateConversationUi();
5746                                    updateRosterUi(UpdateRosterReason.AVATAR);
5747                                }
5748                                if (callback != null) {
5749                                    callback.success(avatar);
5750                                }
5751                                Log.d(
5752                                        Config.LOGTAG,
5753                                        account.getJid().asBareJid()
5754                                                + ": successfully fetched pep avatar for "
5755                                                + avatar.owner);
5756                                return;
5757                            }
5758                        } else {
5759
5760                            Log.d(Config.LOGTAG, ERROR + "(parsing error)");
5761                        }
5762                    } else {
5763                        Element error = result.findChild("error");
5764                        if (error == null) {
5765                            Log.d(Config.LOGTAG, ERROR + "(server error)");
5766                        } else {
5767                            Log.d(Config.LOGTAG, ERROR + error.toString());
5768                        }
5769                    }
5770                    if (callback != null) {
5771                        callback.error(0, null);
5772                    }
5773                });
5774    }
5775
5776    private void fetchAvatarVcard(
5777            final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5778        final Iq packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
5779        this.sendIqPacket(
5780                account,
5781                packet,
5782                response -> {
5783                    final boolean previouslyOmittedPepFetch;
5784                    synchronized (mInProgressAvatarFetches) {
5785                        final String KEY = generateFetchKey(account, avatar);
5786                        mInProgressAvatarFetches.remove(KEY);
5787                        previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY);
5788                    }
5789                    if (response.getType() == Iq.Type.RESULT) {
5790                        Element vCard = response.findChild("vCard", "vcard-temp");
5791                        Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
5792                        String image = photo != null ? photo.findChildContent("BINVAL") : null;
5793                        if (image != null) {
5794                            avatar.image = image;
5795                            if (getFileBackend().save(avatar)) {
5796                                Log.d(
5797                                        Config.LOGTAG,
5798                                        account.getJid().asBareJid()
5799                                                + ": successfully fetched vCard avatar for "
5800                                                + avatar.owner
5801                                                + " omittedPep="
5802                                                + previouslyOmittedPepFetch);
5803                                if (avatar.owner.isBareJid()) {
5804                                    if (account.getJid().asBareJid().equals(avatar.owner)
5805                                            && account.getAvatar() == null) {
5806                                        Log.d(
5807                                                Config.LOGTAG,
5808                                                account.getJid().asBareJid()
5809                                                        + ": had no avatar. replacing with vcard");
5810                                        account.setAvatar(avatar.getFilename());
5811                                        databaseBackend.updateAccount(account);
5812                                        getAvatarService().clear(account);
5813                                        updateAccountUi();
5814                                    } else {
5815                                        final Contact contact =
5816                                                account.getRoster().getContact(avatar.owner);
5817                                        contact.setAvatar(avatar, previouslyOmittedPepFetch);
5818                                        syncRoster(account);
5819                                        getAvatarService().clear(contact);
5820                                        updateRosterUi(UpdateRosterReason.AVATAR);
5821                                    }
5822                                    updateConversationUi();
5823                                } else {
5824                                    Conversation conversation =
5825                                            find(account, avatar.owner.asBareJid());
5826                                    if (conversation != null
5827                                            && conversation.getMode() == Conversation.MODE_MULTI) {
5828                                        MucOptions.User user =
5829                                                conversation
5830                                                        .getMucOptions()
5831                                                        .findUserByFullJid(avatar.owner);
5832                                        if (user != null) {
5833                                            if (user.setAvatar(avatar)) {
5834                                                getAvatarService().clear(user);
5835                                                updateConversationUi();
5836                                                updateMucRosterUi();
5837                                            }
5838                                            if (user.getRealJid() != null) {
5839                                                Contact contact =
5840                                                        account.getRoster()
5841                                                                .getContact(user.getRealJid());
5842                                                contact.setAvatar(avatar);
5843                                                syncRoster(account);
5844                                                getAvatarService().clear(contact);
5845                                                updateRosterUi(UpdateRosterReason.AVATAR);
5846                                            }
5847                                        }
5848                                    }
5849                                }
5850                            }
5851                        }
5852                    }
5853                });
5854    }
5855
5856    public void checkForAvatar(final Account account, final UiCallback<Avatar> callback) {
5857        final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
5858        this.sendIqPacket(
5859                account,
5860                packet,
5861                response -> {
5862                    if (response.getType() != Iq.Type.RESULT) {
5863                        callback.error(0, null);
5864                    }
5865                    final var pubsub = packet.getExtension(PubSub.class);
5866                    if (pubsub == null) {
5867                        callback.error(0, null);
5868                        return;
5869                    }
5870                    final var items = pubsub.getItems();
5871                    if (items == null) {
5872                        callback.error(0, null);
5873                        return;
5874                    }
5875                    final var item = items.getFirstItemWithId(Metadata.class);
5876                    if (item == null) {
5877                        callback.error(0, null);
5878                        return;
5879                    }
5880                    final var avatar = Avatar.parseMetadata(item.getKey(), item.getValue());
5881                    if (avatar == null) {
5882                        callback.error(0, null);
5883                        return;
5884                    }
5885                    avatar.owner = account.getJid().asBareJid();
5886                    if (fileBackend.isAvatarCached(avatar)) {
5887                        if (account.setAvatar(avatar.getFilename())) {
5888                            databaseBackend.updateAccount(account);
5889                        }
5890                        getAvatarService().clear(account);
5891                        callback.success(avatar);
5892                    } else {
5893                        fetchAvatarPep(account, avatar, callback);
5894                    }
5895                });
5896    }
5897
5898    public void notifyAccountAvatarHasChanged(final Account account) {
5899        final XmppConnection connection = account.getXmppConnection();
5900        if (connection != null && connection.getFeatures().bookmarksConversion()) {
5901            Log.d(
5902                    Config.LOGTAG,
5903                    account.getJid().asBareJid()
5904                            + ": avatar changed. resending presence to online group chats");
5905            for (Conversation conversation : conversations) {
5906                if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) {
5907                    presenceToMuc(conversation);
5908                }
5909            }
5910        }
5911    }
5912
5913    public void fetchVcard4(Account account, final Contact contact, final Consumer<Element> callback) {
5914        final var packet = this.mIqGenerator.retrieveVcard4(contact.getJid());
5915        sendIqPacket(account, packet, (result) -> {
5916            if (result.getType() == Iq.Type.RESULT) {
5917                final Element item = IqParser.getItem(result);
5918                if (item != null) {
5919                    final Element vcard4 = item.findChild("vcard", Namespace.VCARD4);
5920                    if (vcard4 != null) {
5921                        if (callback != null) {
5922                            callback.accept(vcard4);
5923                        }
5924                        return;
5925                    }
5926                }
5927            } else {
5928                Element error = result.findChild("error");
5929                if (error == null) {
5930                    Log.d(Config.LOGTAG, "fetchVcard4 (server error)");
5931                } else {
5932                    Log.d(Config.LOGTAG, "fetchVcard4 " + error.toString());
5933                }
5934            }
5935            if (callback != null) {
5936                callback.accept(null);
5937            }
5938
5939        });
5940    }
5941
5942    public void deleteContactOnServer(Contact contact) {
5943        contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
5944        contact.resetOption(Contact.Options.DIRTY_PUSH);
5945        contact.setOption(Contact.Options.DIRTY_DELETE);
5946        Account account = contact.getAccount();
5947        if (account.getStatus() == Account.State.ONLINE) {
5948            final Iq iq = new Iq(Iq.Type.SET);
5949            Element item = iq.query(Namespace.ROSTER).addChild("item");
5950            item.setAttribute("jid", contact.getJid());
5951            item.setAttribute("subscription", "remove");
5952            account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
5953        }
5954    }
5955
5956    public void updateConversation(final Conversation conversation) {
5957        mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation));
5958    }
5959
5960    private void reconnectAccount(
5961            final Account account, final boolean force, final boolean interactive) {
5962        synchronized (account) {
5963            final XmppConnection existingConnection = account.getXmppConnection();
5964            final XmppConnection connection;
5965            if (existingConnection != null) {
5966                connection = existingConnection;
5967            } else if (account.isConnectionEnabled()) {
5968                connection = createConnection(account);
5969                account.setXmppConnection(connection);
5970            } else {
5971                return;
5972            }
5973            final boolean hasInternet = hasInternetConnection();
5974            if (account.isConnectionEnabled() && hasInternet) {
5975                if (!force) {
5976                    disconnect(account, false);
5977                }
5978                Thread thread = new Thread(connection);
5979                connection.setInteractive(interactive);
5980                connection.prepareNewConnection();
5981                connection.interrupt();
5982                thread.start();
5983                scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
5984            } else {
5985                disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
5986                account.getRoster().clearPresences();
5987                connection.resetEverything();
5988                final AxolotlService axolotlService = account.getAxolotlService();
5989                if (axolotlService != null) {
5990                    axolotlService.resetBrokenness();
5991                }
5992                if (!hasInternet) {
5993                    account.setStatus(Account.State.NO_INTERNET);
5994                }
5995            }
5996        }
5997    }
5998
5999    public void reconnectAccountInBackground(final Account account) {
6000        new Thread(() -> reconnectAccount(account, false, true)).start();
6001    }
6002
6003    public void invite(final Conversation conversation, final Jid contact) {
6004        Log.d(
6005                Config.LOGTAG,
6006                conversation.getAccount().getJid().asBareJid()
6007                        + ": inviting "
6008                        + contact
6009                        + " to "
6010                        + conversation.getJid().asBareJid());
6011        final MucOptions.User user =
6012                conversation.getMucOptions().findUserByRealJid(contact.asBareJid());
6013        if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) {
6014            changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null);
6015        }
6016        final var packet = mMessageGenerator.invite(conversation, contact);
6017        sendMessagePacket(conversation.getAccount(), packet);
6018    }
6019
6020    public void directInvite(Conversation conversation, Jid jid) {
6021        final var packet = mMessageGenerator.directInvite(conversation, jid);
6022        sendMessagePacket(conversation.getAccount(), packet);
6023    }
6024
6025    public void resetSendingToWaiting(Account account) {
6026        for (Conversation conversation : getConversations()) {
6027            if (conversation.getAccount() == account) {
6028                conversation.findUnsentTextMessages(
6029                        message -> markMessage(message, Message.STATUS_WAITING));
6030            }
6031        }
6032    }
6033
6034    public Message markMessage(
6035            final Account account, final Jid recipient, final String uuid, final int status) {
6036        return markMessage(account, recipient, uuid, status, null);
6037    }
6038
6039    public Message markMessage(
6040            final Account account,
6041            final Jid recipient,
6042            final String uuid,
6043            final int status,
6044            String errorMessage) {
6045        if (uuid == null) {
6046            return null;
6047        }
6048        for (Conversation conversation : getConversations()) {
6049            if (conversation.getJid().asBareJid().equals(recipient)
6050                    && conversation.getAccount() == account) {
6051                final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
6052                if (message != null) {
6053                    markMessage(message, status, errorMessage);
6054                }
6055                return message;
6056            }
6057        }
6058        return null;
6059    }
6060
6061    public boolean markMessage(
6062            final Conversation conversation,
6063            final String uuid,
6064            final int status,
6065            final String serverMessageId) {
6066        return markMessage(conversation, uuid, status, serverMessageId, null, null, null, null, null);
6067    }
6068
6069    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) {
6070        if (uuid == null) {
6071            return false;
6072        } else {
6073            final Message message = conversation.findSentMessageWithUuid(uuid);
6074            if (message != null) {
6075                if (message.getServerMsgId() == null) {
6076                    message.setServerMsgId(serverMessageId);
6077                }
6078                if (message.getEncryption() == Message.ENCRYPTION_NONE && (body != null || html != null || subject != null || thread != null || attachments != null)) {
6079                    message.setBody(body.content);
6080                    if (body.count > 1) {
6081                        message.setBodyLanguage(body.language);
6082                    }
6083                    message.setHtml(html);
6084                    message.setSubject(subject);
6085                    message.setThread(thread);
6086                    if (attachments != null && attachments.isEmpty()) {
6087                        message.setRelativeFilePath(null);
6088                        message.resetFileParams();
6089                    }
6090                    markMessage(message, status, null, true);
6091                } else {
6092                    markMessage(message, status);
6093                }
6094                return true;
6095            } else {
6096                return false;
6097            }
6098        }
6099    }
6100
6101    public void markMessage(Message message, int status) {
6102        markMessage(message, status, null);
6103    }
6104
6105    public void markMessage(final Message message, final int status, final String errorMessage) {
6106        markMessage(message, status, errorMessage, false);
6107    }
6108
6109    public void markMessage(
6110            final Message message,
6111            final int status,
6112            final String errorMessage,
6113            final boolean includeBody) {
6114        final int oldStatus = message.getStatus();
6115        if (status == Message.STATUS_SEND_FAILED
6116                && (oldStatus == Message.STATUS_SEND_RECEIVED
6117                        || oldStatus == Message.STATUS_SEND_DISPLAYED)) {
6118            return;
6119        }
6120        if (status == Message.STATUS_SEND_RECEIVED && oldStatus == Message.STATUS_SEND_DISPLAYED) {
6121            return;
6122        }
6123        message.setErrorMessage(errorMessage);
6124        message.setStatus(status);
6125        databaseBackend.updateMessage(message, includeBody);
6126        updateConversationUi();
6127        if (oldStatus != status && status == Message.STATUS_SEND_FAILED) {
6128            mNotificationService.pushFailedDelivery(message);
6129        }
6130    }
6131
6132    public SharedPreferences getPreferences() {
6133        return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
6134    }
6135
6136    public long getAutomaticMessageDeletionDate() {
6137        final long timeout =
6138                getLongPreference(
6139                        AppSettings.AUTOMATIC_MESSAGE_DELETION,
6140                        R.integer.automatic_message_deletion);
6141        return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000));
6142    }
6143
6144    public long getLongPreference(String name, @IntegerRes int res) {
6145        long defaultValue = getResources().getInteger(res);
6146        try {
6147            return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue)));
6148        } catch (NumberFormatException e) {
6149            return defaultValue;
6150        }
6151    }
6152
6153    public boolean getBooleanPreference(String name, @BoolRes int res) {
6154        return getPreferences().getBoolean(name, getResources().getBoolean(res));
6155    }
6156
6157    public String getStringPreference(String name, @BoolRes int res) {
6158        return getPreferences().getString(name, getResources().getString(res));
6159    }
6160
6161    public boolean confirmMessages() {
6162        return getBooleanPreference("confirm_messages", R.bool.confirm_messages);
6163    }
6164
6165    public boolean allowMessageCorrection() {
6166        return getBooleanPreference("allow_message_correction", R.bool.allow_message_correction);
6167    }
6168
6169    public boolean sendChatStates() {
6170        return getBooleanPreference("chat_states", R.bool.chat_states);
6171    }
6172
6173    public boolean useTorToConnect() {
6174        return getBooleanPreference("use_tor", R.bool.use_tor);
6175    }
6176
6177    public boolean broadcastLastActivity() {
6178        return getBooleanPreference(AppSettings.BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
6179    }
6180
6181    public int unreadCount() {
6182        int count = 0;
6183        for (Conversation conversation : getConversations()) {
6184            count += conversation.unreadCount(this);
6185        }
6186        return count;
6187    }
6188
6189    private <T> List<T> threadSafeList(Set<T> set) {
6190        synchronized (LISTENER_LOCK) {
6191            return set.isEmpty() ? Collections.emptyList() : new ArrayList<>(set);
6192        }
6193    }
6194
6195    public void showErrorToastInUi(int resId) {
6196        for (OnShowErrorToast listener : threadSafeList(this.mOnShowErrorToasts)) {
6197            listener.onShowErrorToast(resId);
6198        }
6199    }
6200
6201    public void updateConversationUi() {
6202        updateConversationUi(false);
6203    }
6204
6205    public void updateConversationUi(boolean newCaps) {
6206        for (OnConversationUpdate listener : threadSafeList(this.mOnConversationUpdates)) {
6207            listener.onConversationUpdate(newCaps);
6208        }
6209    }
6210
6211    public void notifyJingleRtpConnectionUpdate(
6212            final Account account,
6213            final Jid with,
6214            final String sessionId,
6215            final RtpEndUserState state) {
6216        for (OnJingleRtpConnectionUpdate listener :
6217                threadSafeList(this.onJingleRtpConnectionUpdate)) {
6218            listener.onJingleRtpConnectionUpdate(account, with, sessionId, state);
6219        }
6220    }
6221
6222    public void notifyJingleRtpConnectionUpdate(
6223            CallIntegration.AudioDevice selectedAudioDevice,
6224            Set<CallIntegration.AudioDevice> availableAudioDevices) {
6225        for (OnJingleRtpConnectionUpdate listener :
6226                threadSafeList(this.onJingleRtpConnectionUpdate)) {
6227            listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
6228        }
6229    }
6230
6231    public void updateAccountUi() {
6232        for (final OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) {
6233            listener.onAccountUpdate();
6234        }
6235    }
6236
6237    public void updateRosterUi(final UpdateRosterReason reason) {
6238        if (reason == UpdateRosterReason.PRESENCE) throw new IllegalArgumentException("PRESENCE must also come with a contact");
6239        updateRosterUi(reason, null);
6240    }
6241
6242    public void updateRosterUi(final UpdateRosterReason reason, final Contact contact) {
6243        for (OnRosterUpdate listener : threadSafeList(this.mOnRosterUpdates)) {
6244            listener.onRosterUpdate(reason, contact);
6245        }
6246    }
6247
6248    public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
6249        if (mOnCaptchaRequested.size() > 0) {
6250            DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
6251            Bitmap scaled =
6252                    Bitmap.createScaledBitmap(
6253                            captcha,
6254                            (int) (captcha.getWidth() * metrics.scaledDensity),
6255                            (int) (captcha.getHeight() * metrics.scaledDensity),
6256                            false);
6257            for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
6258                listener.onCaptchaRequested(account, id, data, scaled);
6259            }
6260            return true;
6261        }
6262        return false;
6263    }
6264
6265    public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
6266        for (OnUpdateBlocklist listener : threadSafeList(this.mOnUpdateBlocklist)) {
6267            listener.OnUpdateBlocklist(status);
6268        }
6269    }
6270
6271    public void updateMucRosterUi() {
6272        for (OnMucRosterUpdate listener : threadSafeList(this.mOnMucRosterUpdate)) {
6273            listener.onMucRosterUpdate();
6274        }
6275    }
6276
6277    public void keyStatusUpdated(AxolotlService.FetchStatus report) {
6278        for (OnKeyStatusUpdated listener : threadSafeList(this.mOnKeyStatusUpdated)) {
6279            listener.onKeyStatusUpdated(report);
6280        }
6281    }
6282
6283    public Account findAccountByJid(final Jid jid) {
6284        for (final Account account : this.accounts) {
6285            if (account.getJid().asBareJid().equals(jid.asBareJid())) {
6286                return account;
6287            }
6288        }
6289        return null;
6290    }
6291
6292    public Account findAccountByUuid(final String uuid) {
6293        for (Account account : this.accounts) {
6294            if (account.getUuid().equals(uuid)) {
6295                return account;
6296            }
6297        }
6298        return null;
6299    }
6300
6301    public Conversation findConversationByUuid(String uuid) {
6302        for (Conversation conversation : getConversations()) {
6303            if (conversation.getUuid().equals(uuid)) {
6304                return conversation;
6305            }
6306        }
6307        return null;
6308    }
6309
6310    public Conversation findUniqueConversationByJid(XmppUri xmppUri) {
6311        List<Conversation> findings = new ArrayList<>();
6312        for (Conversation c : getConversations()) {
6313            if (c.getAccount().isEnabled()
6314                    && c.getJid().asBareJid().equals(xmppUri.getJid().asBareJid())
6315                    && ((c.getMode() == Conversational.MODE_MULTI)
6316                            == xmppUri.isAction(XmppUri.ACTION_JOIN))) {
6317                findings.add(c);
6318            }
6319        }
6320        return findings.size() == 1 ? findings.get(0) : null;
6321    }
6322
6323    public boolean markRead(final Conversation conversation, boolean dismiss) {
6324        return markRead(conversation, null, dismiss).size() > 0;
6325    }
6326
6327    public void markRead(final Conversation conversation) {
6328        markRead(conversation, null, true);
6329    }
6330
6331    public List<Message> markRead(
6332            final Conversation conversation, String upToUuid, boolean dismiss) {
6333        if (dismiss) {
6334            mNotificationService.clear(conversation);
6335        }
6336        final List<Message> readMessages = conversation.markRead(upToUuid);
6337        if (readMessages.size() > 0) {
6338            Runnable runnable =
6339                    () -> {
6340                        for (Message message : readMessages) {
6341                            databaseBackend.updateMessage(message, false);
6342                        }
6343                    };
6344            mDatabaseWriterExecutor.execute(runnable);
6345            updateConversationUi();
6346            updateUnreadCountBadge();
6347            return readMessages;
6348        } else {
6349            return readMessages;
6350        }
6351    }
6352
6353    public void markNotificationDismissed(final List<Message> messages) {
6354        Runnable runnable = () -> {
6355            for (final var message : messages) {
6356                message.markNotificationDismissed();
6357                databaseBackend.updateMessage(message, false);
6358            }
6359        };
6360        mDatabaseWriterExecutor.execute(runnable);
6361    }
6362
6363    public synchronized void updateUnreadCountBadge() {
6364        int count = unreadCount();
6365        if (unreadCount != count) {
6366            Log.d(Config.LOGTAG, "update unread count to " + count);
6367            if (count > 0) {
6368                ShortcutBadger.applyCount(getApplicationContext(), count);
6369            } else {
6370                ShortcutBadger.removeCount(getApplicationContext());
6371            }
6372            unreadCount = count;
6373        }
6374    }
6375
6376    public void sendReadMarker(final Conversation conversation, final String upToUuid) {
6377        final boolean isPrivateAndNonAnonymousMuc =
6378                conversation.getMode() == Conversation.MODE_MULTI
6379                        && conversation.isPrivateAndNonAnonymous();
6380        final List<Message> readMessages = this.markRead(conversation, upToUuid, true);
6381        if (readMessages.isEmpty()) {
6382            return;
6383        }
6384        final var account = conversation.getAccount();
6385        final var connection = account.getXmppConnection();
6386        updateConversationUi();
6387        final var last =
6388                Iterables.getLast(
6389                        Collections2.filter(
6390                                readMessages,
6391                                m ->
6392                                        !m.isPrivateMessage()
6393                                                && m.getStatus() == Message.STATUS_RECEIVED),
6394                        null);
6395        if (last == null) {
6396            return;
6397        }
6398
6399        final boolean sendDisplayedMarker =
6400                confirmMessages()
6401                        && (last.trusted() || isPrivateAndNonAnonymousMuc)
6402                        && last.getRemoteMsgId() != null
6403                        && (last.markable || isPrivateAndNonAnonymousMuc);
6404        final boolean serverAssist =
6405                connection != null && connection.getFeatures().mdsServerAssist();
6406
6407        final String stanzaId = last.getServerMsgId();
6408
6409        if (sendDisplayedMarker && serverAssist) {
6410            final var mdsDisplayed = mIqGenerator.mdsDisplayed(stanzaId, conversation);
6411            final var packet = mMessageGenerator.confirm(last);
6412            packet.addChild(mdsDisplayed);
6413            if (!last.isPrivateMessage()) {
6414                packet.setTo(packet.getTo().asBareJid());
6415            }
6416            Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server assisted " + packet);
6417            this.sendMessagePacket(account, packet);
6418        } else {
6419            publishMds(last);
6420            // read markers will be sent after MDS to flush the CSI stanza queue
6421            if (sendDisplayedMarker) {
6422                Log.d(
6423                        Config.LOGTAG,
6424                        conversation.getAccount().getJid().asBareJid()
6425                                + ": sending displayed marker to "
6426                                + last.getCounterpart().toString());
6427                final var packet = mMessageGenerator.confirm(last);
6428                this.sendMessagePacket(account, packet);
6429            }
6430        }
6431    }
6432
6433    private void publishMds(@Nullable final Message message) {
6434        final String stanzaId = message == null ? null : message.getServerMsgId();
6435        if (Strings.isNullOrEmpty(stanzaId)) {
6436            return;
6437        }
6438        final Conversation conversation;
6439        final var conversational = message.getConversation();
6440        if (conversational instanceof Conversation c) {
6441            conversation = c;
6442        } else {
6443            return;
6444        }
6445        final var account = conversation.getAccount();
6446        final var connection = account.getXmppConnection();
6447        if (connection == null || !connection.getFeatures().mds()) {
6448            return;
6449        }
6450        final Jid itemId;
6451        if (message.isPrivateMessage()) {
6452            itemId = message.getCounterpart();
6453        } else {
6454            itemId = conversation.getJid().asBareJid();
6455        }
6456        Log.d(Config.LOGTAG, "publishing mds for " + itemId + "/" + stanzaId);
6457        publishMds(account, itemId, stanzaId, conversation);
6458    }
6459
6460    private void publishMds(
6461            final Account account,
6462            final Jid itemId,
6463            final String stanzaId,
6464            final Conversation conversation) {
6465        final var item = mIqGenerator.mdsDisplayed(stanzaId, conversation);
6466        pushNodeAndEnforcePublishOptions(
6467                account,
6468                Namespace.MDS_DISPLAYED,
6469                item,
6470                itemId.toString(),
6471                PublishOptions.persistentWhitelistAccessMaxItems());
6472    }
6473
6474    public boolean sendReactions(final Message message, final Collection<String> reactions) {
6475        if (message.isPrivateMessage()) throw new IllegalArgumentException("Reactions to PM not implemented");
6476        if (message.getConversation() instanceof Conversation conversation) {
6477            final var isPrivateMessage = message.isPrivateMessage();
6478            final Jid reactTo;
6479            final boolean typeGroupChat;
6480            final String reactToId;
6481            final Collection<Reaction> combinedReactions;
6482            final var newReactions = new HashSet<>(reactions);
6483            newReactions.removeAll(message.getAggregatedReactions().ourReactions);
6484            if (conversation.getMode() == Conversational.MODE_MULTI && !isPrivateMessage) {
6485                final var mucOptions = conversation.getMucOptions();
6486                if (!mucOptions.participating()) {
6487                    Log.e(Config.LOGTAG, "not participating in MUC");
6488                    return false;
6489                }
6490                final var self = mucOptions.getSelf();
6491                final String occupantId = self.getOccupantId();
6492                if (Strings.isNullOrEmpty(occupantId)) {
6493                    Log.e(Config.LOGTAG, "occupant id not found for reaction in MUC");
6494                    return false;
6495                }
6496                final var existingRaw =
6497                        ImmutableSet.copyOf(
6498                                Collections2.transform(message.getReactions(), r -> r.reaction));
6499                final var reactionsAsExistingVariants =
6500                        ImmutableSet.copyOf(
6501                                Collections2.transform(
6502                                        reactions, r -> Emoticons.existingVariant(r, existingRaw)));
6503                if (!reactions.equals(reactionsAsExistingVariants)) {
6504                    Log.d(Config.LOGTAG, "modified reactions to existing variants");
6505                }
6506                reactToId = message.getServerMsgId();
6507                reactTo = conversation.getJid().asBareJid();
6508                typeGroupChat = true;
6509                combinedReactions =
6510                        Reaction.withMine(
6511                                message.getReactions(),
6512                                reactionsAsExistingVariants,
6513                                false,
6514                                self.getFullJid(),
6515                                conversation.getAccount().getJid(),
6516                                occupantId,
6517                                null);
6518            } else {
6519                if (message.isCarbon() || message.getStatus() == Message.STATUS_RECEIVED) {
6520                    reactToId = message.getRemoteMsgId();
6521                } else {
6522                    reactToId = message.getUuid();
6523                }
6524                typeGroupChat = false;
6525                if (isPrivateMessage) {
6526                    reactTo = message.getCounterpart();
6527                } else {
6528                    reactTo = conversation.getJid().asBareJid();
6529                }
6530                combinedReactions =
6531                        Reaction.withFrom(
6532                                message.getReactions(),
6533                                reactions,
6534                                false,
6535                                conversation.getAccount().getJid(),
6536                                null);
6537            }
6538            if (reactTo == null || Strings.isNullOrEmpty(reactToId)) {
6539                Log.e(Config.LOGTAG, "could not find id to react to");
6540                return false;
6541            }
6542
6543            final var packet =
6544                    mMessageGenerator.reaction(reactTo, typeGroupChat, message, reactToId, reactions);
6545
6546            final var quote = QuoteHelper.quote(MessageUtils.prepareQuote(message)) + "\n";
6547            final var body  = quote + String.join(" ", newReactions);
6548            if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && newReactions.size() > 0) {
6549                FILE_ATTACHMENT_EXECUTOR.execute(() -> {
6550                    XmppAxolotlMessage axolotlMessage = conversation.getAccount().getAxolotlService().encrypt(body, conversation);
6551                    packet.setAxolotlMessage(axolotlMessage.toElement());
6552                    packet.addChild("encryption", "urn:xmpp:eme:0")
6553                        .setAttribute("name", "OMEMO")
6554                        .setAttribute("namespace", AxolotlService.PEP_PREFIX);
6555                    sendMessagePacket(conversation.getAccount(), packet);
6556                    message.setReactions(combinedReactions);
6557                    updateMessage(message, false);
6558                });
6559            } else if (conversation.getNextEncryption() == Message.ENCRYPTION_NONE || newReactions.size() < 1) {
6560                if (newReactions.size() > 0) {
6561                    packet.setBody(body);
6562
6563                    packet.addChild("reply", "urn:xmpp:reply:0")
6564                        .setAttribute("to", message.getCounterpart())
6565                        .setAttribute("id", reactToId);
6566                    final var replyFallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reply:0");
6567                    replyFallback.addChild("body", "urn:xmpp:fallback:0")
6568                        .setAttribute("start", "0")
6569                        .setAttribute("end", "" + quote.codePointCount(0, quote.length()));
6570
6571                    final var fallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reactions:0");
6572                    fallback.addChild("body", "urn:xmpp:fallback:0");
6573                }
6574
6575                sendMessagePacket(conversation.getAccount(), packet);
6576                message.setReactions(combinedReactions);
6577                updateMessage(message, false);
6578            }
6579
6580            return true;
6581        } else {
6582            return false;
6583        }
6584    }
6585
6586    public MemorizingTrustManager getMemorizingTrustManager() {
6587        return this.mMemorizingTrustManager;
6588    }
6589
6590    public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
6591        this.mMemorizingTrustManager = trustManager;
6592    }
6593
6594    public void updateMemorizingTrustManager() {
6595        final MemorizingTrustManager trustManager;
6596        if (appSettings.isTrustSystemCAStore()) {
6597            trustManager = new MemorizingTrustManager(getApplicationContext());
6598        } else {
6599            trustManager = new MemorizingTrustManager(getApplicationContext(), null);
6600        }
6601        setMemorizingTrustManager(trustManager);
6602    }
6603
6604    public LruCache<String, Drawable> getDrawableCache() {
6605        return this.mDrawableCache;
6606    }
6607
6608    public Collection<String> getKnownHosts() {
6609        final Set<String> hosts = new HashSet<>();
6610        for (final Account account : getAccounts()) {
6611            hosts.add(account.getServer());
6612            for (final Contact contact : account.getRoster().getContacts()) {
6613                if (contact.showInRoster()) {
6614                    final String server = contact.getServer();
6615                    if (server != null) {
6616                        hosts.add(server);
6617                    }
6618                }
6619            }
6620        }
6621        if (Config.QUICKSY_DOMAIN != null) {
6622            hosts.remove(
6623                    Config.QUICKSY_DOMAIN
6624                            .toString()); // we only want to show this when we type a e164
6625            // number
6626        }
6627        if (Config.MAGIC_CREATE_DOMAIN != null) {
6628            hosts.add(Config.MAGIC_CREATE_DOMAIN);
6629        }
6630        hosts.add("chat.above.im");
6631        return hosts;
6632    }
6633
6634    public Collection<String> getKnownConferenceHosts() {
6635        final Set<String> mucServers = new HashSet<>();
6636        for (final Account account : accounts) {
6637            if (account.getXmppConnection() != null) {
6638                mucServers.addAll(account.getXmppConnection().getMucServers());
6639                for (final Bookmark bookmark : account.getBookmarks()) {
6640                    final Jid jid = bookmark.getJid();
6641                    final String s = jid == null ? null : jid.getDomain().toString();
6642                    if (s != null) {
6643                        mucServers.add(s);
6644                    }
6645                }
6646            }
6647        }
6648        return mucServers;
6649    }
6650
6651    public void sendMessagePacket(
6652            final Account account,
6653            final im.conversations.android.xmpp.model.stanza.Message packet) {
6654        final XmppConnection connection = account.getXmppConnection();
6655        if (connection != null) {
6656            connection.sendMessagePacket(packet);
6657        }
6658    }
6659
6660    public void sendPresencePacket(
6661            final Account account,
6662            final im.conversations.android.xmpp.model.stanza.Presence packet) {
6663        final XmppConnection connection = account.getXmppConnection();
6664        if (connection != null) {
6665            connection.sendPresencePacket(packet);
6666        }
6667    }
6668
6669    public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
6670        final XmppConnection connection = account.getXmppConnection();
6671        if (connection == null) {
6672            return;
6673        }
6674        connection.sendCreateAccountWithCaptchaPacket(id, data);
6675    }
6676
6677    public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback) {
6678        sendIqPacket(account, packet, callback, null);
6679    }
6680
6681    public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback, Long timeout) {
6682        final XmppConnection connection = account.getXmppConnection();
6683        if (connection != null) {
6684            connection.sendIqPacket(packet, callback, timeout);
6685        } else if (callback != null) {
6686            callback.accept(Iq.TIMEOUT);
6687        }
6688    }
6689
6690    public void sendPresence(final Account account) {
6691        sendPresence(account, checkListeners() && broadcastLastActivity());
6692    }
6693
6694    private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
6695        final Presence.Status status;
6696        if (manuallyChangePresence()) {
6697            status = account.getPresenceStatus();
6698        } else {
6699            status = getTargetPresence();
6700        }
6701        final var packet = mPresenceGenerator.selfPresence(account, status);
6702        if (mLastActivity > 0 && includeIdleTimestamp) {
6703            long since =
6704                    Math.min(mLastActivity, System.currentTimeMillis()); // don't send future dates
6705            packet.addChild("idle", Namespace.IDLE)
6706                    .setAttribute("since", AbstractGenerator.getTimestamp(since));
6707        }
6708        sendPresencePacket(account, packet);
6709    }
6710
6711    private void deactivateGracePeriod() {
6712        for (Account account : getAccounts()) {
6713            account.deactivateGracePeriod();
6714        }
6715    }
6716
6717    public void refreshAllPresences() {
6718        boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
6719        for (Account account : getAccounts()) {
6720            if (account.isConnectionEnabled()) {
6721                sendPresence(account, includeIdleTimestamp);
6722            }
6723        }
6724    }
6725
6726    private void refreshAllFcmTokens() {
6727        for (Account account : getAccounts()) {
6728            if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
6729                mPushManagementService.registerPushTokenOnServer(account);
6730            }
6731        }
6732    }
6733
6734    private void sendOfflinePresence(final Account account) {
6735        Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence");
6736        sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
6737    }
6738
6739    public MessageGenerator getMessageGenerator() {
6740        return this.mMessageGenerator;
6741    }
6742
6743    public PresenceGenerator getPresenceGenerator() {
6744        return this.mPresenceGenerator;
6745    }
6746
6747    public IqGenerator getIqGenerator() {
6748        return this.mIqGenerator;
6749    }
6750
6751    public JingleConnectionManager getJingleConnectionManager() {
6752        return this.mJingleConnectionManager;
6753    }
6754
6755    private boolean hasJingleRtpConnection(final Account account) {
6756        return this.mJingleConnectionManager.hasJingleRtpConnection(account);
6757    }
6758
6759    public MessageArchiveService getMessageArchiveService() {
6760        return this.mMessageArchiveService;
6761    }
6762
6763    public QuickConversationsService getQuickConversationsService() {
6764        return this.mQuickConversationsService;
6765    }
6766
6767    public List<Contact> findContacts(Jid jid, String accountJid) {
6768        ArrayList<Contact> contacts = new ArrayList<>();
6769        for (Account account : getAccounts()) {
6770            if ((account.isEnabled() || accountJid != null)
6771                    && (accountJid == null
6772                            || accountJid.equals(account.getJid().asBareJid().toString()))) {
6773                Contact contact = account.getRoster().getContactFromContactList(jid);
6774                if (contact != null) {
6775                    contacts.add(contact);
6776                }
6777            }
6778        }
6779        return contacts;
6780    }
6781
6782    public Conversation findFirstMuc(Jid jid) {
6783        return findFirstMuc(jid, null);
6784    }
6785
6786    public Conversation findFirstMuc(Jid jid, String accountJid) {
6787        for (Conversation conversation : getConversations()) {
6788            if ((conversation.getAccount().isEnabled() || accountJid != null)
6789                    && (accountJid == null || accountJid.equals(conversation.getAccount().getJid().asBareJid().toString()))
6790                    && conversation.getJid().asBareJid().equals(jid.asBareJid()) && conversation.getMode() == Conversation.MODE_MULTI) {
6791                return conversation;
6792            }
6793        }
6794        return null;
6795    }
6796
6797    public NotificationService getNotificationService() {
6798        return this.mNotificationService;
6799    }
6800
6801    public HttpConnectionManager getHttpConnectionManager() {
6802        return this.mHttpConnectionManager;
6803    }
6804
6805    public void resendFailedMessages(final Message message, final boolean forceP2P) {
6806        message.setTime(System.currentTimeMillis());
6807        markMessage(message, Message.STATUS_WAITING);
6808        this.sendMessage(message, true, false, false, forceP2P, null);
6809        if (message.getConversation() instanceof Conversation c) {
6810            c.sort();
6811        }
6812        updateConversationUi();
6813    }
6814
6815    public void clearConversationHistory(final Conversation conversation) {
6816        final long clearDate;
6817        final String reference;
6818        if (conversation.countMessages() > 0) {
6819            Message latestMessage = conversation.getLatestMessage();
6820            clearDate = latestMessage.getTimeSent() + 1000;
6821            reference = latestMessage.getServerMsgId();
6822        } else {
6823            clearDate = System.currentTimeMillis();
6824            reference = null;
6825        }
6826        conversation.clearMessages();
6827        conversation.setHasMessagesLeftOnServer(false); // avoid messages getting loaded through mam
6828        conversation.setLastClearHistory(clearDate, reference);
6829        Runnable runnable =
6830                () -> {
6831                    databaseBackend.deleteMessagesInConversation(conversation);
6832                    databaseBackend.updateConversation(conversation);
6833                };
6834        mDatabaseWriterExecutor.execute(runnable);
6835    }
6836
6837    public boolean sendBlockRequest(
6838            final Blockable blockable, final boolean reportSpam, final String serverMsgId) {
6839        if (blockable != null && blockable.getBlockedJid() != null) {
6840            final var account = blockable.getAccount();
6841            final Jid jid = blockable.getBlockedJid();
6842            this.sendIqPacket(
6843                    account,
6844                    getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId),
6845                    (response) -> {
6846                        if (response.getType() == Iq.Type.RESULT) {
6847                            account.getBlocklist().add(jid);
6848                            updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
6849                        }
6850                    });
6851            if (blockable.getBlockedJid().isFullJid()) {
6852                return false;
6853            } else if (removeBlockedConversations(blockable.getAccount(), jid)) {
6854                updateConversationUi();
6855                return true;
6856            } else {
6857                return false;
6858            }
6859        } else {
6860            return false;
6861        }
6862    }
6863
6864    public boolean removeBlockedConversations(final Account account, final Jid blockedJid) {
6865        boolean removed = false;
6866        synchronized (this.conversations) {
6867            boolean domainJid = blockedJid.getLocal() == null;
6868            for (Conversation conversation : this.conversations) {
6869                boolean jidMatches =
6870                        (domainJid
6871                                        && blockedJid
6872                                                .getDomain()
6873                                                .equals(conversation.getJid().getDomain()))
6874                                || blockedJid.equals(conversation.getJid().asBareJid());
6875                if (conversation.getAccount() == account
6876                        && conversation.getMode() == Conversation.MODE_SINGLE
6877                        && jidMatches) {
6878                    this.conversations.remove(conversation);
6879                    markRead(conversation);
6880                    conversation.setStatus(Conversation.STATUS_ARCHIVED);
6881                    Log.d(
6882                            Config.LOGTAG,
6883                            account.getJid().asBareJid()
6884                                    + ": archiving conversation "
6885                                    + conversation.getJid().asBareJid()
6886                                    + " because jid was blocked");
6887                    updateConversation(conversation);
6888                    removed = true;
6889                }
6890            }
6891        }
6892        return removed;
6893    }
6894
6895    public void sendUnblockRequest(final Blockable blockable) {
6896        if (blockable != null && blockable.getJid() != null) {
6897            final var account = blockable.getAccount();
6898            final Jid jid = blockable.getBlockedJid();
6899            this.sendIqPacket(
6900                    account,
6901                    getIqGenerator().generateSetUnblockRequest(jid),
6902                    response -> {
6903                        if (response.getType() == Iq.Type.RESULT) {
6904                            account.getBlocklist().remove(jid);
6905                            updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
6906                        }
6907                    });
6908        }
6909    }
6910
6911    public void publishDisplayName(final Account account) {
6912        String displayName = account.getDisplayName();
6913        final Iq request;
6914        if (TextUtils.isEmpty(displayName)) {
6915            request = mIqGenerator.deleteNode(Namespace.NICK);
6916        } else {
6917            request = mIqGenerator.publishNick(displayName);
6918        }
6919        mAvatarService.clear(account);
6920        sendIqPacket(
6921                account,
6922                request,
6923                (packet) -> {
6924                    if (packet.getType() == Iq.Type.ERROR) {
6925                        Log.d(
6926                                Config.LOGTAG,
6927                                account.getJid().asBareJid()
6928                                        + ": unable to modify nick name "
6929                                        + packet);
6930                    }
6931                });
6932    }
6933
6934    public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String, String> key) {
6935        ServiceDiscoveryResult result = discoCache.get(key);
6936        if (result != null) {
6937            return result;
6938        } else {
6939            if (key.first == null || key.second == null) return null;
6940            result = databaseBackend.findDiscoveryResult(key.first, key.second);
6941            if (result != null) {
6942                discoCache.put(key, result);
6943            }
6944            return result;
6945        }
6946    }
6947
6948    public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) {
6949        final var request = new Iq(input == null ? Iq.Type.GET : Iq.Type.SET);
6950        request.setTo(jid);
6951        Element query = request.query("jabber:iq:gateway");
6952        if (input != null) {
6953            Element prompt = query.addChild("prompt");
6954            prompt.setContent(input);
6955        }
6956        sendIqPacket(account, request, packet -> {
6957            if (packet.getType() == Iq.Type.RESULT) {
6958                callback.onGatewayResult(packet.query().findChildContent(input == null ? "prompt" : "jid"), null);
6959            } else {
6960                Element error = packet.findChild("error");
6961                callback.onGatewayResult(null, error == null ? null : error.findChildContent("text"));
6962            }
6963        });
6964    }
6965
6966    public void fetchCaps(Account account, final Jid jid, final Presence presence) {
6967        fetchCaps(account, jid, presence, null);
6968    }
6969
6970    public void fetchCaps(Account account, final Jid jid, final Presence presence, Runnable cb) {
6971        final Pair<String, String> key = presence == null ? null : new Pair<>(presence.getHash(), presence.getVer());
6972        final ServiceDiscoveryResult disco = key == null ? null : getCachedServiceDiscoveryResult(key);
6973
6974        if (disco != null) {
6975            presence.setServiceDiscoveryResult(disco);
6976            final Contact contact = account.getRoster().getContact(jid);
6977            if (contact.refreshRtpCapability()) {
6978                syncRoster(account);
6979            }
6980            contact.refreshCaps();
6981            if (disco.hasIdentity("gateway", "pstn")) {
6982                contact.registerAsPhoneAccount(this);
6983                mQuickConversationsService.considerSyncBackground(false);
6984            }
6985            updateConversationUi(true);
6986        } else {
6987            final Iq request = new Iq(Iq.Type.GET);
6988            request.setTo(jid);
6989            final String node = presence == null ? null : presence.getNode();
6990            final String ver = presence == null ? null : presence.getVer();
6991            final Element query = request.query(Namespace.DISCO_INFO);
6992            if (node != null && ver != null) {
6993                query.setAttribute("node", node + "#" + ver);
6994            }
6995
6996            Log.d(
6997                    Config.LOGTAG,
6998                    account.getJid().asBareJid()
6999                            + ": making disco request for "
7000                            + (key == null ? null : key.second)
7001                            + " to "
7002                            + jid);
7003            sendIqPacket(
7004                    account,
7005                    request,
7006                    (response) -> {
7007                        if (response.getType() == Iq.Type.RESULT) {
7008                            final ServiceDiscoveryResult discoveryResult =
7009                                    new ServiceDiscoveryResult(response);
7010                            if (presence == null || presence.getVer() == null || presence.getVer().equals(discoveryResult.getVer())) {
7011                                databaseBackend.insertDiscoveryResult(discoveryResult);
7012                                injectServiceDiscoveryResult(
7013                                        account.getRoster(),
7014                                        presence == null ? null : presence.getHash(),
7015                                        presence == null ? null : presence.getVer(),
7016                                        jid.getResource(),
7017                                        discoveryResult);
7018                                if (discoveryResult.hasIdentity("gateway", "pstn")) {
7019                                    final Contact contact = account.getRoster().getContact(jid);
7020                                    contact.registerAsPhoneAccount(this);
7021                                    mQuickConversationsService.considerSyncBackground(false);
7022                                }
7023                                updateConversationUi(true);
7024                                if (cb != null) cb.run();
7025                            } else {
7026                                Log.d(
7027                                        Config.LOGTAG,
7028                                        account.getJid().asBareJid()
7029                                                + ": mismatch in caps for contact "
7030                                                + jid
7031                                                + " "
7032                                                + presence.getVer()
7033                                                + " vs "
7034                                                + discoveryResult.getVer());
7035                            }
7036                        } else {
7037                            Log.d(
7038                                    Config.LOGTAG,
7039                                    account.getJid().asBareJid()
7040                                            + ": unable to fetch caps from "
7041                                            + jid);
7042                        }
7043                    });
7044        }
7045    }
7046
7047    public void fetchCommands(Account account, final Jid jid, Consumer<Iq> callback) {
7048        final var request = mIqGenerator.queryDiscoItems(jid, "http://jabber.org/protocol/commands");
7049        sendIqPacket(account, request, callback);
7050    }
7051
7052    private void injectServiceDiscoveryResult(
7053            Roster roster, String hash, String ver, String resource, ServiceDiscoveryResult disco) {
7054        boolean rosterNeedsSync = false;
7055        for (final Contact contact : roster.getContacts()) {
7056            boolean serviceDiscoverySet = false;
7057            Presence onePresence = contact.getPresences().get(resource == null ? "" : resource);
7058            if (onePresence != null) {
7059                onePresence.setServiceDiscoveryResult(disco);
7060                serviceDiscoverySet = true;
7061            } else if (resource == null && hash == null && ver == null) {
7062                Presence p = new Presence(Presence.Status.OFFLINE, null, null, null, "");
7063                p.setServiceDiscoveryResult(disco);
7064                contact.updatePresence("", p);
7065                serviceDiscoverySet = true;
7066            }
7067            if (hash != null && ver != null) {
7068                for (final Presence presence : contact.getPresences().getPresences()) {
7069                    if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
7070                        presence.setServiceDiscoveryResult(disco);
7071                        serviceDiscoverySet = true;
7072                    }
7073                }
7074            }
7075            if (serviceDiscoverySet) {
7076                rosterNeedsSync |= contact.refreshRtpCapability();
7077                contact.refreshCaps();
7078            }
7079        }
7080        if (rosterNeedsSync) {
7081            syncRoster(roster.getAccount());
7082        }
7083    }
7084
7085    public void fetchMamPreferences(final Account account, final OnMamPreferencesFetched callback) {
7086        final MessageArchiveService.Version version = MessageArchiveService.Version.get(account);
7087        final Iq request = new Iq(Iq.Type.GET);
7088        request.addChild("prefs", version.namespace);
7089        sendIqPacket(
7090                account,
7091                request,
7092                (packet) -> {
7093                    final Element prefs = packet.findChild("prefs", version.namespace);
7094                    if (packet.getType() == Iq.Type.RESULT && prefs != null) {
7095                        callback.onPreferencesFetched(prefs);
7096                    } else {
7097                        callback.onPreferencesFetchFailed();
7098                    }
7099                });
7100    }
7101
7102    public PushManagementService getPushManagementService() {
7103        return mPushManagementService;
7104    }
7105
7106    public void changeStatus(Account account, PresenceTemplate template, String signature) {
7107        if (!template.getStatusMessage().isEmpty()) {
7108            databaseBackend.insertPresenceTemplate(template);
7109        }
7110        account.setPgpSignature(signature);
7111        account.setPresenceStatus(template.getStatus());
7112        account.setPresenceStatusMessage(template.getStatusMessage());
7113        databaseBackend.updateAccount(account);
7114        sendPresence(account);
7115    }
7116
7117    public List<PresenceTemplate> getPresenceTemplates(Account account) {
7118        List<PresenceTemplate> templates = databaseBackend.getPresenceTemplates();
7119        for (PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) {
7120            if (!templates.contains(template)) {
7121                templates.add(0, template);
7122            }
7123        }
7124        return templates;
7125    }
7126
7127    public void saveConversationAsBookmark(final Conversation conversation, final String name) {
7128        final Account account = conversation.getAccount();
7129        final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid());
7130        String nick = conversation.getMucOptions().getActualNick();
7131        if (nick == null) nick = conversation.getJid().getResource();
7132        if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
7133            bookmark.setNick(nick);
7134        }
7135        if (!TextUtils.isEmpty(name)) {
7136            bookmark.setBookmarkName(name);
7137        }
7138        bookmark.setAutojoin(true);
7139        createBookmark(account, bookmark);
7140        bookmark.setConversation(conversation);
7141    }
7142
7143    public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
7144        boolean performedVerification = false;
7145        final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
7146        for (XmppUri.Fingerprint fp : fingerprints) {
7147            if (fp.type == XmppUri.FingerprintType.OMEMO) {
7148                String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
7149                FingerprintStatus fingerprintStatus =
7150                        axolotlService.getFingerprintTrust(fingerprint);
7151                if (fingerprintStatus != null) {
7152                    if (!fingerprintStatus.isVerified()) {
7153                        performedVerification = true;
7154                        axolotlService.setFingerprintTrust(
7155                                fingerprint, fingerprintStatus.toVerified());
7156                    }
7157                } else {
7158                    axolotlService.preVerifyFingerprint(contact, fingerprint);
7159                }
7160            }
7161        }
7162        return performedVerification;
7163    }
7164
7165    public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
7166        final AxolotlService axolotlService = account.getAxolotlService();
7167        boolean verifiedSomething = false;
7168        for (XmppUri.Fingerprint fp : fingerprints) {
7169            if (fp.type == XmppUri.FingerprintType.OMEMO) {
7170                String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
7171                Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint);
7172                FingerprintStatus fingerprintStatus =
7173                        axolotlService.getFingerprintTrust(fingerprint);
7174                if (fingerprintStatus != null) {
7175                    if (!fingerprintStatus.isVerified()) {
7176                        axolotlService.setFingerprintTrust(
7177                                fingerprint, fingerprintStatus.toVerified());
7178                        verifiedSomething = true;
7179                    }
7180                } else {
7181                    axolotlService.preVerifyFingerprint(account, fingerprint);
7182                    verifiedSomething = true;
7183                }
7184            }
7185        }
7186        return verifiedSomething;
7187    }
7188
7189    public ShortcutService getShortcutService() {
7190        return mShortcutService;
7191    }
7192
7193    public void pushMamPreferences(Account account, Element prefs) {
7194        final Iq set = new Iq(Iq.Type.SET);
7195        set.addChild(prefs);
7196        account.setMamPrefs(prefs);
7197        sendIqPacket(account, set, null);
7198    }
7199
7200    public void evictPreview(File f) {
7201        if (f == null) return;
7202
7203        if (mDrawableCache.remove(f.getAbsolutePath()) != null) {
7204            Log.d(Config.LOGTAG, "deleted cached preview");
7205        }
7206    }
7207
7208    public void evictPreview(String uuid) {
7209        if (mDrawableCache.remove(uuid) != null) {
7210            Log.d(Config.LOGTAG, "deleted cached preview");
7211        }
7212    }
7213
7214    public interface OnMamPreferencesFetched {
7215        void onPreferencesFetched(Element prefs);
7216
7217        void onPreferencesFetchFailed();
7218    }
7219
7220    public interface OnAccountCreated {
7221        void onAccountCreated(Account account);
7222
7223        void informUser(int r);
7224    }
7225
7226    public interface OnMoreMessagesLoaded {
7227        void onMoreMessagesLoaded(int count, Conversation conversation);
7228
7229        void informUser(int r);
7230    }
7231
7232    public interface OnAccountPasswordChanged {
7233        void onPasswordChangeSucceeded();
7234
7235        void onPasswordChangeFailed();
7236    }
7237
7238    public interface OnRoomDestroy {
7239        void onRoomDestroySucceeded();
7240
7241        void onRoomDestroyFailed();
7242    }
7243
7244    public interface OnAffiliationChanged {
7245        void onAffiliationChangedSuccessful(Jid jid);
7246
7247        void onAffiliationChangeFailed(Jid jid, int resId);
7248    }
7249
7250    public interface OnConversationUpdate {
7251        default void onConversationUpdate() { onConversationUpdate(false); }
7252        default void onConversationUpdate(boolean newCaps) { onConversationUpdate(); }
7253    }
7254
7255    public interface OnJingleRtpConnectionUpdate {
7256        void onJingleRtpConnectionUpdate(
7257                final Account account,
7258                final Jid with,
7259                final String sessionId,
7260                final RtpEndUserState state);
7261
7262        void onAudioDeviceChanged(
7263                CallIntegration.AudioDevice selectedAudioDevice,
7264                Set<CallIntegration.AudioDevice> availableAudioDevices);
7265    }
7266
7267    public interface OnAccountUpdate {
7268        void onAccountUpdate();
7269    }
7270
7271    public interface OnCaptchaRequested {
7272        void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha);
7273    }
7274
7275    public interface OnRosterUpdate {
7276        void onRosterUpdate(final UpdateRosterReason reason, final Contact contact);
7277    }
7278
7279    public interface OnMucRosterUpdate {
7280        void onMucRosterUpdate();
7281    }
7282
7283    public interface OnConferenceConfigurationFetched {
7284        void onConferenceConfigurationFetched(Conversation conversation);
7285
7286        void onFetchFailed(Conversation conversation, String errorCondition);
7287    }
7288
7289    public interface OnConferenceJoined {
7290        void onConferenceJoined(Conversation conversation);
7291    }
7292
7293    public interface OnConfigurationPushed {
7294        void onPushSucceeded();
7295
7296        void onPushFailed();
7297    }
7298
7299    public interface OnShowErrorToast {
7300        void onShowErrorToast(int resId);
7301    }
7302
7303    public class XmppConnectionBinder extends Binder {
7304        public XmppConnectionService getService() {
7305            return XmppConnectionService.this;
7306        }
7307    }
7308
7309    private class InternalEventReceiver extends BroadcastReceiver {
7310
7311        @Override
7312        public void onReceive(final Context context, final Intent intent) {
7313            onStartCommand(intent, 0, 0);
7314        }
7315    }
7316
7317    private class RestrictedEventReceiver extends BroadcastReceiver {
7318
7319        private final Collection<String> allowedActions;
7320
7321        private RestrictedEventReceiver(final Collection<String> allowedActions) {
7322            this.allowedActions = allowedActions;
7323        }
7324
7325        @Override
7326        public void onReceive(final Context context, final Intent intent) {
7327            final String action = intent == null ? null : intent.getAction();
7328            if (allowedActions.contains(action)) {
7329                onStartCommand(intent, 0, 0);
7330            } else {
7331                Log.e(Config.LOGTAG, "restricting broadcast of event " + action);
7332            }
7333        }
7334    }
7335
7336    public static class OngoingCall {
7337        public final AbstractJingleConnection.Id id;
7338        public final Set<Media> media;
7339        public final boolean reconnecting;
7340
7341        public OngoingCall(
7342                AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
7343            this.id = id;
7344            this.media = media;
7345            this.reconnecting = reconnecting;
7346        }
7347
7348        @Override
7349        public boolean equals(Object o) {
7350            if (this == o) return true;
7351            if (o == null || getClass() != o.getClass()) return false;
7352            OngoingCall that = (OngoingCall) o;
7353            return reconnecting == that.reconnecting
7354                    && Objects.equal(id, that.id)
7355                    && Objects.equal(media, that.media);
7356        }
7357
7358        @Override
7359        public int hashCode() {
7360            return Objects.hashCode(id, media, reconnecting);
7361        }
7362    }
7363
7364    public static void toggleForegroundService(final XmppConnectionService service) {
7365        if (service == null) {
7366            return;
7367        }
7368        service.toggleForegroundService();
7369    }
7370
7371    public static void toggleForegroundService(final ConversationsActivity activity) {
7372        if (activity == null) {
7373            return;
7374        }
7375        toggleForegroundService(activity.xmppConnectionService);
7376    }
7377
7378    public static class BlockedMediaException extends Exception { }
7379
7380    public static enum UpdateRosterReason {
7381        INIT,
7382        AVATAR,
7383        PUSH,
7384        PRESENCE
7385    }
7386}