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