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