XmppConnectionService.java

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