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