XmppConnectionService.java

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