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