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