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