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