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