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