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