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