XmppConnectionService.java

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