XmppConnectionService.java

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