XmppConnectionService.java

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