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