1package eu.siacs.conversations.services;
2
3import static eu.siacs.conversations.utils.Compatibility.s;
4import static eu.siacs.conversations.utils.Random.SECURE_RANDOM;
5
6import android.Manifest;
7import android.annotation.SuppressLint;
8import android.app.AlarmManager;
9import android.app.KeyguardManager;
10import android.app.Notification;
11import android.app.NotificationManager;
12import android.app.PendingIntent;
13import android.app.Service;
14import android.content.BroadcastReceiver;
15import android.content.ComponentName;
16import android.content.Context;
17import android.content.Intent;
18import android.content.IntentFilter;
19import android.content.SharedPreferences;
20import android.content.pm.PackageManager;
21import android.content.pm.ServiceInfo;
22import android.database.ContentObserver;
23import android.graphics.Bitmap;
24import android.graphics.drawable.AnimatedImageDrawable;
25import android.graphics.drawable.BitmapDrawable;
26import android.graphics.drawable.Drawable;
27import android.media.AudioManager;
28import android.net.ConnectivityManager;
29import android.net.Network;
30import android.net.NetworkCapabilities;
31import android.net.NetworkInfo;
32import android.net.Uri;
33import android.os.Binder;
34import android.os.Build;
35import android.os.Bundle;
36import android.os.Environment;
37import android.os.IBinder;
38import android.os.Messenger;
39import android.os.PowerManager;
40import android.os.PowerManager.WakeLock;
41import android.os.SystemClock;
42import android.preference.PreferenceManager;
43import android.provider.ContactsContract;
44import android.provider.DocumentsContract;
45import android.security.KeyChain;
46import android.text.TextUtils;
47import android.util.DisplayMetrics;
48import android.util.Log;
49import android.util.LruCache;
50import android.util.Pair;
51import androidx.annotation.BoolRes;
52import androidx.annotation.IntegerRes;
53import androidx.annotation.NonNull;
54import androidx.annotation.Nullable;
55import androidx.core.app.RemoteInput;
56import androidx.core.content.ContextCompat;
57
58import com.cheogram.android.EmojiSearch;
59import com.cheogram.android.WebxdcUpdate;
60
61import com.google.common.base.Objects;
62import com.google.common.base.Optional;
63import com.google.common.base.Strings;
64import com.google.common.collect.Collections2;
65import com.google.common.collect.ImmutableMap;
66import com.google.common.collect.ImmutableSet;
67import com.google.common.collect.Iterables;
68import com.google.common.collect.Maps;
69import com.google.common.collect.Multimap;
70import com.google.common.io.Files;
71
72import com.kedia.ogparser.JsoupProxy;
73import com.kedia.ogparser.OpenGraphCallback;
74import com.kedia.ogparser.OpenGraphParser;
75import com.kedia.ogparser.OpenGraphResult;
76
77import org.conscrypt.Conscrypt;
78import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep;
79import org.openintents.openpgp.IOpenPgpService2;
80import org.openintents.openpgp.util.OpenPgpApi;
81import org.openintents.openpgp.util.OpenPgpServiceConnection;
82
83import java.io.File;
84import java.io.FileInputStream;
85import java.io.IOException;
86import java.net.URI;
87import java.security.Security;
88import java.security.cert.CertificateException;
89import java.security.cert.X509Certificate;
90import java.util.ArrayList;
91import java.util.Arrays;
92import java.util.Collection;
93import java.util.Collections;
94import java.util.HashMap;
95import java.util.HashSet;
96import java.util.Hashtable;
97import java.util.Iterator;
98import java.util.List;
99import java.util.ListIterator;
100import java.util.Map;
101import java.util.Set;
102import java.util.WeakHashMap;
103import java.util.concurrent.CopyOnWriteArrayList;
104import java.util.concurrent.CountDownLatch;
105import java.util.concurrent.Executor;
106import java.util.concurrent.Executors;
107import java.util.concurrent.Semaphore;
108import java.util.concurrent.RejectedExecutionException;
109import java.util.concurrent.ScheduledExecutorService;
110import java.util.concurrent.TimeUnit;
111import java.util.concurrent.atomic.AtomicBoolean;
112import java.util.concurrent.atomic.AtomicLong;
113import java.util.concurrent.atomic.AtomicReference;
114import java.util.function.Consumer;
115
116import io.ipfs.cid.Cid;
117
118import com.google.common.util.concurrent.FutureCallback;
119import com.google.common.util.concurrent.Futures;
120import com.google.common.util.concurrent.ListenableFuture;
121import com.google.common.util.concurrent.MoreExecutors;
122import eu.siacs.conversations.AppSettings;
123import eu.siacs.conversations.Config;
124import eu.siacs.conversations.R;
125import eu.siacs.conversations.android.JabberIdContact;
126import eu.siacs.conversations.crypto.OmemoSetting;
127import eu.siacs.conversations.crypto.PgpDecryptionService;
128import eu.siacs.conversations.crypto.PgpEngine;
129import eu.siacs.conversations.crypto.axolotl.AxolotlService;
130import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
131import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
132import eu.siacs.conversations.entities.Account;
133import eu.siacs.conversations.entities.Blockable;
134import eu.siacs.conversations.entities.Bookmark;
135import eu.siacs.conversations.entities.Contact;
136import eu.siacs.conversations.entities.Conversation;
137import eu.siacs.conversations.entities.Conversational;
138import eu.siacs.conversations.entities.DownloadableFile;
139import eu.siacs.conversations.entities.Message;
140import eu.siacs.conversations.entities.MucOptions;
141import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
142import eu.siacs.conversations.entities.PresenceTemplate;
143import eu.siacs.conversations.entities.Reaction;
144import eu.siacs.conversations.generator.AbstractGenerator;
145import eu.siacs.conversations.generator.IqGenerator;
146import eu.siacs.conversations.generator.MessageGenerator;
147import eu.siacs.conversations.generator.PresenceGenerator;
148import eu.siacs.conversations.http.HttpConnectionManager;
149import eu.siacs.conversations.http.ServiceOutageStatus;
150import eu.siacs.conversations.parser.AbstractParser;
151import eu.siacs.conversations.parser.IqParser;
152import eu.siacs.conversations.persistance.DatabaseBackend;
153import eu.siacs.conversations.persistance.FileBackend;
154import eu.siacs.conversations.persistance.UnifiedPushDatabase;
155import eu.siacs.conversations.receiver.SystemEventReceiver;
156import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity;
157import eu.siacs.conversations.ui.ConversationsActivity;
158import eu.siacs.conversations.ui.RtpSessionActivity;
159import eu.siacs.conversations.ui.UiCallback;
160import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
161import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
162import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
163import eu.siacs.conversations.ui.util.QuoteHelper;
164import eu.siacs.conversations.utils.AccountUtils;
165import eu.siacs.conversations.utils.Compatibility;
166import eu.siacs.conversations.utils.ConversationsFileObserver;
167import eu.siacs.conversations.utils.CryptoHelper;
168import eu.siacs.conversations.utils.Emoticons;
169import eu.siacs.conversations.utils.EasyOnboardingInvite;
170import eu.siacs.conversations.utils.ExceptionHelper;
171import eu.siacs.conversations.utils.FileUtils;
172import eu.siacs.conversations.utils.MessageUtils;
173import eu.siacs.conversations.utils.Emoticons;
174import eu.siacs.conversations.utils.MimeUtils;
175import eu.siacs.conversations.utils.PhoneHelper;
176import eu.siacs.conversations.utils.QuickLoader;
177import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
178import eu.siacs.conversations.utils.ReplacingTaskManager;
179import eu.siacs.conversations.utils.Resolver;
180import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
181import eu.siacs.conversations.utils.StringUtils;
182import eu.siacs.conversations.utils.TorServiceUtils;
183import eu.siacs.conversations.utils.ThemeHelper;
184import eu.siacs.conversations.utils.WakeLockHelper;
185import eu.siacs.conversations.utils.XmppUri;
186import eu.siacs.conversations.xml.Element;
187import eu.siacs.conversations.xml.LocalizedContent;
188import eu.siacs.conversations.xml.Namespace;
189import eu.siacs.conversations.xmpp.IqErrorResponseException;
190import eu.siacs.conversations.xmpp.Jid;
191import eu.siacs.conversations.xmpp.OnContactStatusChanged;
192import eu.siacs.conversations.xmpp.OnGatewayResult;
193import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
194import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
195import eu.siacs.conversations.xmpp.OnStatusChanged;
196import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
197import eu.siacs.conversations.xmpp.XmppConnection;
198import eu.siacs.conversations.xmpp.chatstate.ChatState;
199import eu.siacs.conversations.xmpp.forms.Data;
200import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
201import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
202import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
203import eu.siacs.conversations.xmpp.jingle.Media;
204import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
205import eu.siacs.conversations.xmpp.mam.MamReference;
206import eu.siacs.conversations.xmpp.manager.DiscoManager;
207import eu.siacs.conversations.xmpp.pep.Avatar;
208import eu.siacs.conversations.xmpp.pep.PublishOptions;
209import im.conversations.android.xmpp.Entity;
210import im.conversations.android.xmpp.model.avatar.Metadata;
211import im.conversations.android.xmpp.model.bookmark.Storage;
212import im.conversations.android.xmpp.model.disco.info.InfoQuery;
213import im.conversations.android.xmpp.model.mds.Displayed;
214import im.conversations.android.xmpp.model.pubsub.PubSub;
215import im.conversations.android.xmpp.model.stanza.Iq;
216import im.conversations.android.xmpp.model.stanza.Presence;
217import im.conversations.android.xmpp.model.storage.PrivateStorage;
218import java.io.File;
219import java.security.Security;
220import java.security.cert.CertificateException;
221import java.security.cert.X509Certificate;
222import java.util.ArrayList;
223import java.util.Arrays;
224import java.util.Collection;
225import java.util.Collections;
226import java.util.HashSet;
227import java.util.Iterator;
228import java.util.List;
229import java.util.ListIterator;
230import java.util.Map;
231import java.util.Set;
232import java.util.WeakHashMap;
233import java.util.concurrent.CopyOnWriteArrayList;
234import java.util.concurrent.CountDownLatch;
235import java.util.concurrent.Executor;
236import java.util.concurrent.Executors;
237import java.util.concurrent.RejectedExecutionException;
238import java.util.concurrent.ScheduledExecutorService;
239import java.util.concurrent.TimeUnit;
240import java.util.concurrent.TimeoutException;
241import java.util.concurrent.atomic.AtomicBoolean;
242import java.util.concurrent.atomic.AtomicLong;
243import java.util.concurrent.atomic.AtomicReference;
244import java.util.function.Consumer;
245import me.leolin.shortcutbadger.ShortcutBadger;
246import okhttp3.HttpUrl;
247import org.conscrypt.Conscrypt;
248import org.jxmpp.stringprep.libidn.LibIdnXmppStringprep;
249import org.openintents.openpgp.IOpenPgpService2;
250import org.openintents.openpgp.util.OpenPgpApi;
251import org.openintents.openpgp.util.OpenPgpServiceConnection;
252
253import okhttp3.HttpUrl;
254import okhttp3.OkHttpClient;
255
256public class XmppConnectionService extends Service {
257
258 public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations";
259 public static final String ACTION_MARK_AS_READ = "mark_as_read";
260 public static final String ACTION_SNOOZE = "snooze";
261 public static final String ACTION_CLEAR_MESSAGE_NOTIFICATION = "clear_message_notification";
262 public static final String ACTION_CLEAR_MISSED_CALL_NOTIFICATION =
263 "clear_missed_call_notification";
264 public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error";
265 public static final String ACTION_TRY_AGAIN = "try_again";
266
267 public static final String ACTION_TEMPORARILY_DISABLE = "temporarily_disable";
268 public static final String ACTION_PING = "ping";
269 public static final String ACTION_IDLE_PING = "idle_ping";
270 public static final String ACTION_INTERNAL_PING = "internal_ping";
271 public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh";
272 public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received";
273 public static final String ACTION_DISMISS_CALL = "dismiss_call";
274 public static final String ACTION_END_CALL = "end_call";
275 public static final String ACTION_STARTING_CALL = "starting_call";
276 public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
277 public static final String ACTION_CALL_INTEGRATION_SERVICE_STARTED =
278 "call_integration_service_started";
279 private static final String ACTION_POST_CONNECTIVITY_CHANGE =
280 "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
281 public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS =
282 "eu.siacs.conversations.UNIFIED_PUSH_RENEW";
283 public static final String ACTION_QUICK_LOG = "eu.siacs.conversations.QUICK_LOG";
284
285 private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
286
287 public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
288 private static final Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor();
289 public static final Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor();
290
291 private final ScheduledExecutorService internalPingExecutor =
292 Executors.newSingleThreadScheduledExecutor();
293 private static final SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR =
294 new SerialSingleThreadExecutor("VideoCompression");
295 private final SerialSingleThreadExecutor mDatabaseWriterExecutor =
296 new SerialSingleThreadExecutor("DatabaseWriter");
297 private final SerialSingleThreadExecutor mDatabaseReaderExecutor =
298 new SerialSingleThreadExecutor("DatabaseReader");
299 private final SerialSingleThreadExecutor mNotificationExecutor =
300 new SerialSingleThreadExecutor("NotificationExecutor");
301 private final ReplacingTaskManager mRosterSyncTaskManager = new ReplacingTaskManager();
302 private final IBinder mBinder = new XmppConnectionBinder();
303 private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
304 private final IqGenerator mIqGenerator = new IqGenerator(this);
305 private final Set<String> mInProgressAvatarFetches = new HashSet<>();
306 private final Set<String> mOmittedPepAvatarFetches = new HashSet<>();
307 private final HashSet<Jid> mLowPingTimeoutMode = new HashSet<>();
308 private final Consumer<Iq> mDefaultIqHandler =
309 (packet) -> {
310 if (packet.getType() != Iq.Type.RESULT) {
311 final var error = packet.getError();
312 String text = error != null ? error.findChildContent("text") : null;
313 if (text != null) {
314 Log.d(Config.LOGTAG, "received iq error: " + text);
315 }
316 }
317 };
318 public DatabaseBackend databaseBackend;
319 private Multimap<String, String> mutedMucUsers;
320 private final ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger");
321 private final ReplacingSerialSingleThreadExecutor mStickerScanExecutor = new ReplacingSerialSingleThreadExecutor("StickerScan");
322 private long mLastActivity = 0;
323 private long mLastMucPing = 0;
324 private Map<String, Message> mScheduledMessages = new HashMap<>();
325 private long mLastStickerRescan = 0;
326 private final AppSettings appSettings = new AppSettings(this);
327 private final FileBackend fileBackend = new FileBackend(this);
328 private MemorizingTrustManager mMemorizingTrustManager;
329 private final NotificationService mNotificationService = new NotificationService(this);
330 private final UnifiedPushBroker unifiedPushBroker = new UnifiedPushBroker(this);
331 private final ChannelDiscoveryService mChannelDiscoveryService =
332 new ChannelDiscoveryService(this);
333 private final ShortcutService mShortcutService = new ShortcutService(this);
334 private final AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false);
335 private final AtomicBoolean mOngoingVideoTranscoding = new AtomicBoolean(false);
336 private final AtomicBoolean mForceDuringOnCreate = new AtomicBoolean(false);
337 private final AtomicReference<OngoingCall> ongoingCall = new AtomicReference<>();
338 private final MessageGenerator mMessageGenerator = new MessageGenerator(this);
339 public OnContactStatusChanged onContactStatusChanged =
340 (contact, online) -> {
341 final var conversation = find(contact);
342 if (conversation == null) {
343 return;
344 }
345 if (online) {
346 if (contact.getPresences().size() == 1) {
347 sendUnsentMessages(conversation);
348 }
349 }
350 };
351 private final PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
352 private List<Account> accounts;
353 private final JingleConnectionManager mJingleConnectionManager =
354 new JingleConnectionManager(this);
355 private final HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(this);
356 private final AvatarService mAvatarService = new AvatarService(this);
357 private final MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
358 private final PushManagementService mPushManagementService = new PushManagementService(this);
359 private final QuickConversationsService mQuickConversationsService =
360 new QuickConversationsService(this);
361 private final ConversationsFileObserver fileObserver =
362 new ConversationsFileObserver(
363 Environment.getExternalStorageDirectory().getAbsolutePath()) {
364 @Override
365 public void onEvent(final int event, final File file) {
366 markFileDeleted(file);
367 }
368 };
369 private final OnMessageAcknowledged mOnMessageAcknowledgedListener =
370 new OnMessageAcknowledged() {
371
372 @Override
373 public boolean onMessageAcknowledged(
374 final Account account, final Jid to, final String id) {
375 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
376 final String sessionId =
377 id.substring(
378 JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX
379 .length());
380 mJingleConnectionManager.updateProposedSessionDiscovered(
381 account,
382 to,
383 sessionId,
384 JingleConnectionManager.DeviceDiscoveryState
385 .SEARCHING_ACKNOWLEDGED);
386 }
387
388 final Jid bare = to.asBareJid();
389
390 for (final Conversation conversation : getConversations()) {
391 if (conversation.getAccount() == account
392 && conversation.getJid().asBareJid().equals(bare)) {
393 final Message message = conversation.findUnsentMessageWithUuid(id);
394 if (message != null) {
395 message.setStatus(Message.STATUS_SEND);
396 message.setErrorMessage(null);
397 databaseBackend.updateMessage(message, false);
398 return true;
399 }
400 }
401 }
402 return false;
403 }
404 };
405
406 private final AtomicBoolean diallerIntegrationActive = new AtomicBoolean(false);
407
408 public void setDiallerIntegrationActive(boolean active) {
409 diallerIntegrationActive.set(active);
410 }
411
412 private boolean destroyed = false;
413
414 private int unreadCount = -1;
415
416 // Ui callback listeners
417 private final Set<OnConversationUpdate> mOnConversationUpdates =
418 Collections.newSetFromMap(new WeakHashMap<OnConversationUpdate, Boolean>());
419 private final Set<OnShowErrorToast> mOnShowErrorToasts =
420 Collections.newSetFromMap(new WeakHashMap<OnShowErrorToast, Boolean>());
421 private final Set<OnAccountUpdate> mOnAccountUpdates =
422 Collections.newSetFromMap(new WeakHashMap<OnAccountUpdate, Boolean>());
423 private final Set<OnCaptchaRequested> mOnCaptchaRequested =
424 Collections.newSetFromMap(new WeakHashMap<OnCaptchaRequested, Boolean>());
425 private final Set<OnRosterUpdate> mOnRosterUpdates =
426 Collections.newSetFromMap(new WeakHashMap<OnRosterUpdate, Boolean>());
427 private final Set<OnUpdateBlocklist> mOnUpdateBlocklist =
428 Collections.newSetFromMap(new WeakHashMap<OnUpdateBlocklist, Boolean>());
429 private final Set<OnMucRosterUpdate> mOnMucRosterUpdate =
430 Collections.newSetFromMap(new WeakHashMap<OnMucRosterUpdate, Boolean>());
431 private final Set<OnKeyStatusUpdated> mOnKeyStatusUpdated =
432 Collections.newSetFromMap(new WeakHashMap<OnKeyStatusUpdated, Boolean>());
433 private final Set<OnJingleRtpConnectionUpdate> onJingleRtpConnectionUpdate =
434 Collections.newSetFromMap(new WeakHashMap<OnJingleRtpConnectionUpdate, Boolean>());
435
436 private final Object LISTENER_LOCK = new Object();
437
438 public final Set<String> FILENAMES_TO_IGNORE_DELETION = new HashSet<>();
439
440 private final AtomicLong mLastExpiryRun = new AtomicLong(0);
441 private final OnStatusChanged statusListener =
442 new OnStatusChanged() {
443
444 @Override
445 public void onStatusChanged(final Account account) {
446 final var status = account.getStatus();
447 if (ServiceOutageStatus.isPossibleOutage(status)) {
448 fetchServiceOutageStatus(account);
449 }
450 XmppConnection connection = account.getXmppConnection();
451 updateAccountUi();
452
453 if (account.getStatus() == Account.State.ONLINE
454 || account.getStatus().isError()) {
455 mQuickConversationsService.signalAccountStateChange();
456 }
457
458 if (account.getStatus() == Account.State.ONLINE) {
459 synchronized (mLowPingTimeoutMode) {
460 if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
461 Log.d(
462 Config.LOGTAG,
463 account.getJid().asBareJid()
464 + ": leaving low ping timeout mode");
465 }
466 }
467 if (account.setShowErrorNotification(true)) {
468 databaseBackend.updateAccount(account);
469 }
470 mMessageArchiveService.executePendingQueries(account);
471 if (connection != null && connection.getFeatures().csi()) {
472 if (checkListeners()) {
473 Log.d(
474 Config.LOGTAG,
475 account.getJid().asBareJid() + " sending csi//inactive");
476 connection.sendInactive();
477 } else {
478 Log.d(
479 Config.LOGTAG,
480 account.getJid().asBareJid() + " sending csi//active");
481 connection.sendActive();
482 }
483 }
484 List<Conversation> conversations = getConversations();
485 for (Conversation conversation : conversations) {
486 final boolean inProgressJoin;
487 synchronized (account.inProgressConferenceJoins) {
488 inProgressJoin =
489 account.inProgressConferenceJoins.contains(conversation);
490 }
491 final boolean pendingJoin;
492 synchronized (account.pendingConferenceJoins) {
493 pendingJoin = account.pendingConferenceJoins.contains(conversation);
494 }
495 if (conversation.getAccount() == account
496 && !pendingJoin
497 && !inProgressJoin) {
498 sendUnsentMessages(conversation);
499 }
500 }
501 final List<Conversation> pendingLeaves;
502 synchronized (account.pendingConferenceLeaves) {
503 pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves);
504 account.pendingConferenceLeaves.clear();
505 }
506 for (Conversation conversation : pendingLeaves) {
507 leaveMuc(conversation);
508 }
509 final List<Conversation> pendingJoins;
510 synchronized (account.pendingConferenceJoins) {
511 pendingJoins = new ArrayList<>(account.pendingConferenceJoins);
512 account.pendingConferenceJoins.clear();
513 }
514 for (Conversation conversation : pendingJoins) {
515 joinMuc(conversation);
516 }
517 scheduleWakeUpCall(
518 Config.PING_MAX_INTERVAL * 1000L, account.getUuid().hashCode());
519 } else if (account.getStatus() == Account.State.OFFLINE
520 || account.getStatus() == Account.State.DISABLED
521 || account.getStatus() == Account.State.LOGGED_OUT) {
522 resetSendingToWaiting(account);
523 if (account.isConnectionEnabled() && isInLowPingTimeoutMode(account)) {
524 Log.d(
525 Config.LOGTAG,
526 account.getJid().asBareJid()
527 + ": went into offline state during low ping mode."
528 + " reconnecting now");
529 reconnectAccount(account, true, false);
530 } else {
531 final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2;
532 scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
533 }
534 } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
535 databaseBackend.updateAccount(account);
536 reconnectAccount(account, true, false);
537 } else if (account.getStatus() != Account.State.CONNECTING
538 && account.getStatus() != Account.State.NO_INTERNET) {
539 resetSendingToWaiting(account);
540 if (connection != null && account.getStatus().isAttemptReconnect()) {
541 final boolean aggressive =
542 account.getStatus() == Account.State.SEE_OTHER_HOST
543 || hasJingleRtpConnection(account);
544 final int next = connection.getTimeToNextAttempt(aggressive);
545 final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account);
546 if (next <= 0) {
547 Log.d(
548 Config.LOGTAG,
549 account.getJid().asBareJid()
550 + ": error connecting account. reconnecting now."
551 + " lowPingTimeout="
552 + lowPingTimeoutMode);
553 reconnectAccount(account, true, false);
554 } else {
555 final int attempt = connection.getAttempt() + 1;
556 Log.d(
557 Config.LOGTAG,
558 account.getJid().asBareJid()
559 + ": error connecting account. try again in "
560 + next
561 + "s for the "
562 + attempt
563 + " time. lowPingTimeout="
564 + lowPingTimeoutMode
565 + ", aggressive="
566 + aggressive);
567 scheduleWakeUpCall(next, account.getUuid().hashCode());
568 if (aggressive) {
569 internalPingExecutor.schedule(
570 XmppConnectionService.this
571 ::manageAccountConnectionStatesInternal,
572 (next * 1000L) + 50,
573 TimeUnit.MILLISECONDS);
574 }
575 }
576 }
577 }
578 getNotificationService().updateErrorNotification();
579 }
580 };
581
582 private OpenPgpServiceConnection pgpServiceConnection;
583 private PgpEngine mPgpEngine = null;
584 private WakeLock wakeLock;
585 private LruCache<String, Drawable> mDrawableCache;
586 private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
587 private final BroadcastReceiver mInternalRestrictedEventReceiver =
588 new RestrictedEventReceiver(List.of(TorServiceUtils.ACTION_STATUS));
589 private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
590 private EmojiSearch emojiSearch = null;
591
592 private static String generateFetchKey(Account account, final Avatar avatar) {
593 return account.getJid().asBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum;
594 }
595
596 private boolean isInLowPingTimeoutMode(Account account) {
597 synchronized (mLowPingTimeoutMode) {
598 return mLowPingTimeoutMode.contains(account.getJid().asBareJid());
599 }
600 }
601
602 public void startOngoingVideoTranscodingForegroundNotification() {
603 mOngoingVideoTranscoding.set(true);
604 toggleForegroundService();
605 }
606
607 public void stopOngoingVideoTranscodingForegroundNotification() {
608 mOngoingVideoTranscoding.set(false);
609 toggleForegroundService();
610 }
611
612 public boolean areMessagesInitialized() {
613 return this.restoredFromDatabaseLatch.getCount() == 0;
614 }
615
616 public PgpEngine getPgpEngine() {
617 if (!Config.supportOpenPgp()) {
618 return null;
619 } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
620 if (this.mPgpEngine == null) {
621 this.mPgpEngine =
622 new PgpEngine(
623 new OpenPgpApi(
624 getApplicationContext(), pgpServiceConnection.getService()),
625 this);
626 }
627 return mPgpEngine;
628 } else {
629 return null;
630 }
631 }
632
633 public OpenPgpApi getOpenPgpApi() {
634 if (!Config.supportOpenPgp()) {
635 return null;
636 } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
637 return new OpenPgpApi(this, pgpServiceConnection.getService());
638 } else {
639 return null;
640 }
641 }
642
643 public AppSettings getAppSettings() {
644 return this.appSettings;
645 }
646
647 public FileBackend getFileBackend() {
648 return this.fileBackend;
649 }
650
651 public DownloadableFile getFileForCid(Cid cid) {
652 return this.databaseBackend.getFileForCid(cid);
653 }
654
655 public String getUrlForCid(Cid cid) {
656 return this.databaseBackend.getUrlForCid(cid);
657 }
658
659 public void saveCid(Cid cid, File file) throws BlockedMediaException {
660 saveCid(cid, file, null);
661 }
662
663 public void saveCid(Cid cid, File file, String url) throws BlockedMediaException {
664 if (this.databaseBackend.isBlockedMedia(cid)) {
665 throw new BlockedMediaException();
666 }
667 this.databaseBackend.saveCid(cid, file, url);
668 }
669
670 public boolean muteMucUser(MucOptions.User user) {
671 boolean muted = databaseBackend.muteMucUser(user);
672 if (!muted) return false;
673 mutedMucUsers.put(user.getMuc().toString(), user.getOccupantId());
674 return true;
675 }
676
677 public boolean unmuteMucUser(MucOptions.User user) {
678 boolean unmuted = databaseBackend.unmuteMucUser(user);
679 if (!unmuted) return false;
680 mutedMucUsers.remove(user.getMuc().toString(), user.getOccupantId());
681 return true;
682 }
683
684 public boolean isMucUserMuted(MucOptions.User user) {
685 return mutedMucUsers.containsEntry("" + user.getMuc(), user.getOccupantId());
686 }
687
688 public void blockMedia(File f) {
689 try {
690 Cid[] cids = getFileBackend().calculateCids(new FileInputStream(f));
691 for (Cid cid : cids) {
692 blockMedia(cid);
693 }
694 } catch (final IOException e) { }
695 }
696
697 public void blockMedia(Cid cid) {
698 this.databaseBackend.blockMedia(cid);
699 }
700
701 public void clearBlockedMedia() {
702 this.databaseBackend.clearBlockedMedia();
703 }
704
705 public Message getMessage(Conversation conversation, String uuid) {
706 return this.databaseBackend.getMessage(conversation, uuid);
707 }
708
709 public Map<String, Message> getMessageFuzzyIds(Conversation conversation, Collection<String> ids) {
710 return this.databaseBackend.getMessageFuzzyIds(conversation, ids);
711 }
712
713 public void insertWebxdcUpdate(final WebxdcUpdate update) {
714 this.databaseBackend.insertWebxdcUpdate(update);
715 }
716
717 public WebxdcUpdate findLastWebxdcUpdate(Message message) {
718 return this.databaseBackend.findLastWebxdcUpdate(message);
719 }
720
721 public List<WebxdcUpdate> findWebxdcUpdates(Message message, long serial) {
722 return this.databaseBackend.findWebxdcUpdates(message, serial);
723 }
724
725 public AvatarService getAvatarService() {
726 return this.mAvatarService;
727 }
728
729 public void attachLocationToConversation(
730 final Conversation conversation, final Uri uri, final String subject, final UiCallback<Message> callback) {
731 int encryption = conversation.getNextEncryption();
732 if (encryption == Message.ENCRYPTION_PGP) {
733 encryption = Message.ENCRYPTION_DECRYPTED;
734 }
735 Message message = new Message(conversation, uri.toString(), encryption);
736 if (subject != null && subject.length() > 0) message.setSubject(subject);
737 message.setThread(conversation.getThread());
738 Message.configurePrivateMessage(message);
739 if (encryption == Message.ENCRYPTION_DECRYPTED) {
740 getPgpEngine().encrypt(message, callback);
741 } else {
742 sendMessage(message);
743 callback.success(message);
744 }
745 }
746
747 public void attachFileToConversation(
748 final Conversation conversation,
749 final Uri uri,
750 final String type,
751 final String subject,
752 final UiCallback<Message> callback) {
753 final Message message;
754 if (conversation.getReplyTo() == null) {
755 message = new Message(conversation, "", conversation.getNextEncryption());
756 } else {
757 message = conversation.getReplyTo().reply();
758 message.setEncryption(conversation.getNextEncryption());
759 }
760 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
761 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
762 }
763 if (subject != null && subject.length() > 0) message.setSubject(subject);
764 message.setThread(conversation.getThread());
765 if (!Message.configurePrivateFileMessage(message)) {
766 message.setCounterpart(conversation.getNextCounterpart());
767 message.setType(Message.TYPE_FILE);
768 }
769 Log.d(Config.LOGTAG, "attachFile: type=" + message.getType());
770 Log.d(Config.LOGTAG, "counterpart=" + message.getCounterpart());
771 final AttachFileToConversationRunnable runnable =
772 new AttachFileToConversationRunnable(this, uri, type, message, callback);
773 if (runnable.isVideoMessage()) {
774 VIDEO_COMPRESSION_EXECUTOR.execute(runnable);
775 } else {
776 FILE_ATTACHMENT_EXECUTOR.execute(runnable);
777 }
778 }
779
780 public void attachImageToConversation(
781 final Conversation conversation,
782 final Uri uri,
783 final String type,
784 final String subject,
785 final UiCallback<Message> callback) {
786 final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type);
787 final String compressPictures = getCompressPicturesPreference();
788
789 if ("never".equals(compressPictures)
790 || ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))
791 || (mimeType != null && mimeType.endsWith("/gif"))
792 || getFileBackend().unusualBounds(uri) || "data".equals(uri.getScheme())) {
793 Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": not compressing picture. sending as file");
794 attachFileToConversation(conversation, uri, mimeType, subject, callback);
795 return;
796 }
797 final Message message;
798
799 if (conversation.getReplyTo() == null) {
800 message = new Message(conversation, "", conversation.getNextEncryption());
801 } else {
802 message = conversation.getReplyTo().reply();
803 message.setEncryption(conversation.getNextEncryption());
804 }
805 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
806 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
807 }
808 if (subject != null && subject.length() > 0) message.setSubject(subject);
809 message.setThread(conversation.getThread());
810 if (!Message.configurePrivateFileMessage(message)) {
811 message.setCounterpart(conversation.getNextCounterpart());
812 message.setType(Message.TYPE_IMAGE);
813 }
814 Log.d(Config.LOGTAG, "attachImage: type=" + message.getType());
815 FILE_ATTACHMENT_EXECUTOR.execute(() -> {
816 try {
817 getFileBackend().copyImageToPrivateStorage(message, uri);
818 } catch (FileBackend.ImageCompressionException e) {
819 Log.d(Config.LOGTAG, "unable to compress image. fall back to file transfer", e);
820 attachFileToConversation(conversation, uri, mimeType, subject, callback);
821 return;
822 } catch (final FileBackend.FileCopyException e) {
823 callback.error(e.getResId(), message);
824 return;
825 }
826 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
827 final PgpEngine pgpEngine = getPgpEngine();
828 if (pgpEngine != null) {
829 pgpEngine.encrypt(message, callback);
830 } else if (callback != null) {
831 callback.error(R.string.unable_to_connect_to_keychain, null);
832 }
833 } else {
834 sendMessage(message, false, false, false, () -> callback.success(message));
835 }
836 });
837 }
838
839 private File stickerDir() {
840 SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(getBaseContext());
841 final String dir = p.getString("sticker_directory", "Stickers");
842 if (dir.startsWith("content://")) {
843 Uri uri = Uri.parse(dir);
844 uri = DocumentsContract.buildDocumentUriUsingTree(uri, DocumentsContract.getTreeDocumentId(uri));
845 return new File(FileUtils.getPath(getBaseContext(), uri));
846 } else {
847 return new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) + "/" + dir);
848 }
849 }
850
851 public void rescanStickers() {
852 long msToRescan = (mLastStickerRescan + 600000L) - SystemClock.elapsedRealtime();
853 if (msToRescan > 0) return;
854 Log.d(Config.LOGTAG, "rescanStickers");
855
856 mLastStickerRescan = SystemClock.elapsedRealtime();
857 mStickerScanExecutor.execute(() -> {
858 Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
859 try {
860 for (File file : Files.fileTraverser().breadthFirst(stickerDir())) {
861 try {
862 if (file.isFile() && file.canRead()) {
863 DownloadableFile df = new DownloadableFile(file.getAbsolutePath());
864 Drawable icon = fileBackend.getThumbnail(df, getResources(), (int) (getResources().getDisplayMetrics().density * 288), false);
865 final String filename = Files.getNameWithoutExtension(df.getName());
866 Cid[] cids = fileBackend.calculateCids(new FileInputStream(df));
867 for (Cid cid : cids) {
868 saveCid(cid, file);
869 }
870 if (file.length() < 129000) {
871 emojiSearch.addEmoji(new EmojiSearch.CustomEmoji(filename, cids[0].toString(), icon, file.getParentFile().getName()));
872 }
873 }
874 } catch (final Exception e) {
875 Log.w(Config.LOGTAG, "rescanStickers: " + e);
876 }
877 }
878 } catch (final Exception e) {
879 Log.w(Config.LOGTAG, "rescanStickers: " + e);
880 }
881 });
882 }
883
884 protected void cleanupCache() {
885 if (Build.VERSION.SDK_INT < 26) return; // Doesn't support file.toPath
886 mStickerScanExecutor.execute(() -> {
887 Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
888 final var now = System.currentTimeMillis();
889 try {
890 for (File file : Files.fileTraverser().breadthFirst(getCacheDir())) {
891 if (file.isFile() && file.canRead() && file.canWrite()) {
892 final var attrs = java.nio.file.Files.readAttributes(file.toPath(), java.nio.file.attribute.BasicFileAttributes.class);
893 if ((now - attrs.lastAccessTime().toMillis()) > 1000L * 60 * 60 * 24 * 10) {
894 Log.d(Config.LOGTAG, "cleanupCache removing file not used recently: " + file);
895 file.delete();
896 }
897 }
898 }
899 } catch (final Exception e) {
900 Log.w(Config.LOGTAG, "cleanupCache " + e);
901 }
902 });
903 }
904
905 public EmojiSearch emojiSearch() {
906 return emojiSearch;
907 }
908
909 public Conversation find(Bookmark bookmark) {
910 return find(bookmark.getAccount(), bookmark.getJid());
911 }
912
913 public Conversation find(final Account account, final Jid jid) {
914 return find(getConversations(), account, jid);
915 }
916
917 public boolean isMuc(final Account account, final Jid jid) {
918 final Conversation c = find(account, jid);
919 return c != null && c.getMode() == Conversational.MODE_MULTI;
920 }
921
922 public void search(
923 final List<String> term,
924 final String uuid,
925 final OnSearchResultsAvailable onSearchResultsAvailable) {
926 MessageSearchTask.search(this, term, uuid, onSearchResultsAvailable);
927 }
928
929 @Override
930 public int onStartCommand(final Intent intent, int flags, int startId) {
931 final var nomedia = getBooleanPreference("nomedia", R.bool.default_nomedia);
932 fileBackend.setupNomedia(nomedia);
933 final String action = Strings.nullToEmpty(intent == null ? null : intent.getAction());
934 final boolean needsForegroundService =
935 intent != null
936 && intent.getBooleanExtra(
937 SystemEventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false);
938 if (needsForegroundService) {
939 Log.d(
940 Config.LOGTAG,
941 "toggle forced foreground service after receiving event (action="
942 + action
943 + ")");
944 toggleForegroundService(true, action.equals(ACTION_STARTING_CALL));
945 }
946 final String uuid = intent == null ? null : intent.getStringExtra("uuid");
947 switch (action) {
948 case QuickConversationsService.SMS_RETRIEVED_ACTION:
949 mQuickConversationsService.handleSmsReceived(intent);
950 break;
951 case ConnectivityManager.CONNECTIVITY_ACTION:
952 if (hasInternetConnection()) {
953 if (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0) {
954 schedulePostConnectivityChange();
955 }
956 if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
957 resetAllAttemptCounts(true, false);
958 }
959 Resolver.clearCache();
960 }
961 break;
962 case Intent.ACTION_SHUTDOWN:
963 logoutAndSave(true);
964 return START_NOT_STICKY;
965 case ACTION_CLEAR_MESSAGE_NOTIFICATION:
966 mNotificationExecutor.execute(
967 () -> {
968 try {
969 final Conversation c = findConversationByUuid(uuid);
970 if (c != null) {
971 mNotificationService.clearMessages(c);
972 } else {
973 mNotificationService.clearMessages();
974 }
975 restoredFromDatabaseLatch.await();
976
977 } catch (InterruptedException e) {
978 Log.d(
979 Config.LOGTAG,
980 "unable to process clear message notification");
981 }
982 });
983 break;
984 case ACTION_CLEAR_MISSED_CALL_NOTIFICATION:
985 mNotificationExecutor.execute(
986 () -> {
987 try {
988 final Conversation c = findConversationByUuid(uuid);
989 if (c != null) {
990 mNotificationService.clearMissedCalls(c);
991 } else {
992 mNotificationService.clearMissedCalls();
993 }
994 restoredFromDatabaseLatch.await();
995
996 } catch (InterruptedException e) {
997 Log.d(
998 Config.LOGTAG,
999 "unable to process clear missed call notification");
1000 }
1001 });
1002 break;
1003 case ACTION_DISMISS_CALL:
1004 {
1005 if (intent == null) {
1006 break;
1007 }
1008 final String sessionId =
1009 intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
1010 Log.d(
1011 Config.LOGTAG,
1012 "received intent to dismiss call with session id " + sessionId);
1013 mJingleConnectionManager.rejectRtpSession(sessionId);
1014 break;
1015 }
1016 case TorServiceUtils.ACTION_STATUS:
1017 final String status =
1018 intent == null ? null : intent.getStringExtra(TorServiceUtils.EXTRA_STATUS);
1019 // TODO port and host are in 'extras' - but this may not be a reliable source?
1020 if ("ON".equals(status)) {
1021 handleOrbotStartedEvent();
1022 return START_STICKY;
1023 }
1024 break;
1025 case ACTION_END_CALL:
1026 {
1027 if (intent == null) {
1028 break;
1029 }
1030 final String sessionId =
1031 intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
1032 Log.d(
1033 Config.LOGTAG,
1034 "received intent to end call with session id " + sessionId);
1035 mJingleConnectionManager.endRtpSession(sessionId);
1036 }
1037 break;
1038 case ACTION_PROVISION_ACCOUNT:
1039 {
1040 if (intent == null) {
1041 break;
1042 }
1043 final String address = intent.getStringExtra("address");
1044 final String password = intent.getStringExtra("password");
1045 if (QuickConversationsService.isQuicksy()
1046 || Strings.isNullOrEmpty(address)
1047 || Strings.isNullOrEmpty(password)) {
1048 break;
1049 }
1050 provisionAccount(address, password);
1051 break;
1052 }
1053 case ACTION_DISMISS_ERROR_NOTIFICATIONS:
1054 dismissErrorNotifications();
1055 break;
1056 case ACTION_TRY_AGAIN:
1057 resetAllAttemptCounts(false, true);
1058 break;
1059 case ACTION_REPLY_TO_CONVERSATION:
1060 final Bundle remoteInput =
1061 intent == null ? null : RemoteInput.getResultsFromIntent(intent);
1062 if (remoteInput == null) {
1063 break;
1064 }
1065 final CharSequence body = remoteInput.getCharSequence("text_reply");
1066 final boolean dismissNotification =
1067 intent.getBooleanExtra("dismiss_notification", false);
1068 final String lastMessageUuid = intent.getStringExtra("last_message_uuid");
1069 if (body == null || body.length() <= 0) {
1070 break;
1071 }
1072 mNotificationExecutor.execute(
1073 () -> {
1074 try {
1075 restoredFromDatabaseLatch.await();
1076 final Conversation c = findConversationByUuid(uuid);
1077 if (c != null) {
1078 directReply(
1079 c,
1080 body.toString(),
1081 lastMessageUuid,
1082 dismissNotification);
1083 }
1084 } catch (InterruptedException e) {
1085 Log.d(Config.LOGTAG, "unable to process direct reply");
1086 }
1087 });
1088 break;
1089 case ACTION_MARK_AS_READ:
1090 mNotificationExecutor.execute(
1091 () -> {
1092 final Conversation c = findConversationByUuid(uuid);
1093 if (c == null) {
1094 Log.d(
1095 Config.LOGTAG,
1096 "received mark read intent for unknown conversation ("
1097 + uuid
1098 + ")");
1099 return;
1100 }
1101 try {
1102 restoredFromDatabaseLatch.await();
1103 sendReadMarker(c, null);
1104 } catch (InterruptedException e) {
1105 Log.d(
1106 Config.LOGTAG,
1107 "unable to process notification read marker for"
1108 + " conversation "
1109 + c.getName());
1110 }
1111 });
1112 break;
1113 case ACTION_SNOOZE:
1114 mNotificationExecutor.execute(
1115 () -> {
1116 final Conversation c = findConversationByUuid(uuid);
1117 if (c == null) {
1118 Log.d(
1119 Config.LOGTAG,
1120 "received snooze intent for unknown conversation ("
1121 + uuid
1122 + ")");
1123 return;
1124 }
1125 c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000);
1126 mNotificationService.clearMessages(c);
1127 updateConversation(c);
1128 });
1129 case AudioManager.RINGER_MODE_CHANGED_ACTION:
1130 case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED:
1131 if (dndOnSilentMode()) {
1132 refreshAllPresences();
1133 }
1134 break;
1135 case Intent.ACTION_SCREEN_ON:
1136 deactivateGracePeriod();
1137 case Intent.ACTION_USER_PRESENT:
1138 case Intent.ACTION_SCREEN_OFF:
1139 if (awayWhenScreenLocked()) {
1140 refreshAllPresences();
1141 }
1142 break;
1143 case ACTION_FCM_TOKEN_REFRESH:
1144 refreshAllFcmTokens();
1145 break;
1146 case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS:
1147 if (intent == null) {
1148 break;
1149 }
1150 final String instance = intent.getStringExtra("instance");
1151 final String application = intent.getStringExtra("application");
1152 final Messenger messenger = intent.getParcelableExtra("messenger");
1153 final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger;
1154 if (messenger != null && application != null && instance != null) {
1155 pushTargetMessenger =
1156 new UnifiedPushBroker.PushTargetMessenger(
1157 new UnifiedPushDatabase.PushTarget(application, instance),
1158 messenger);
1159 Log.d(Config.LOGTAG, "found push target messenger");
1160 } else {
1161 pushTargetMessenger = null;
1162 }
1163 final Optional<UnifiedPushBroker.Transport> transport =
1164 renewUnifiedPushEndpoints(pushTargetMessenger);
1165 if (instance != null && transport.isPresent()) {
1166 unifiedPushBroker.rebroadcastEndpoint(messenger, instance, transport.get());
1167 }
1168 break;
1169 case ACTION_IDLE_PING:
1170 scheduleNextIdlePing();
1171 break;
1172 case ACTION_FCM_MESSAGE_RECEIVED:
1173 Log.d(Config.LOGTAG, "push message arrived in service. account");
1174 break;
1175 case ACTION_QUICK_LOG:
1176 final String message = intent == null ? null : intent.getStringExtra("message");
1177 if (message != null && Config.QUICK_LOG) {
1178 quickLog(message);
1179 }
1180 break;
1181 case Intent.ACTION_SEND:
1182 final Uri uri = intent == null ? null : intent.getData();
1183 if (uri != null) {
1184 Log.d(Config.LOGTAG, "received uri permission for " + uri);
1185 }
1186 return START_STICKY;
1187 case ACTION_TEMPORARILY_DISABLE:
1188 toggleSoftDisabled(true);
1189 if (checkListeners()) {
1190 stopSelf();
1191 }
1192 return START_NOT_STICKY;
1193 }
1194 sendScheduledMessages();
1195 final var extras = intent == null ? null : intent.getExtras();
1196 try {
1197 internalPingExecutor.execute(() -> manageAccountConnectionStates(action, extras));
1198 } catch (final RejectedExecutionException e) {
1199 Log.e(Config.LOGTAG, "can not schedule connection states manager");
1200 }
1201 if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) {
1202 expireOldMessages();
1203 }
1204 return START_STICKY;
1205 }
1206
1207 private void quickLog(final String message) {
1208 if (Strings.isNullOrEmpty(message)) {
1209 return;
1210 }
1211 final Account account = AccountUtils.getFirstEnabled(this);
1212 if (account == null) {
1213 return;
1214 }
1215 final Conversation conversation =
1216 findOrCreateConversation(account, Config.BUG_REPORTS, false, true);
1217 final Message report = new Message(conversation, message, Message.ENCRYPTION_NONE);
1218 report.setStatus(Message.STATUS_RECEIVED);
1219 conversation.add(report);
1220 databaseBackend.createMessage(report);
1221 updateConversationUi();
1222 }
1223
1224 private void manageAccountConnectionStatesInternal() {
1225 manageAccountConnectionStates(ACTION_INTERNAL_PING, null);
1226 }
1227
1228 private synchronized void manageAccountConnectionStates(
1229 final String action, final Bundle extras) {
1230 final String pushedAccountHash = extras == null ? null : extras.getString("account");
1231 final boolean interactive = java.util.Objects.equals(ACTION_TRY_AGAIN, action);
1232 WakeLockHelper.acquire(wakeLock);
1233 boolean pingNow =
1234 ConnectivityManager.CONNECTIVITY_ACTION.equals(action)
1235 || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0
1236 && ACTION_POST_CONNECTIVITY_CHANGE.equals(action));
1237 final HashSet<Account> pingCandidates = new HashSet<>();
1238 final String androidId = pushedAccountHash == null ? null : PhoneHelper.getAndroidId(this);
1239 for (final Account account : accounts) {
1240 final boolean pushWasMeantForThisAccount =
1241 androidId != null
1242 && CryptoHelper.getAccountFingerprint(account, androidId)
1243 .equals(pushedAccountHash);
1244 pingNow |=
1245 processAccountState(
1246 account,
1247 interactive,
1248 "ui".equals(action),
1249 pushWasMeantForThisAccount,
1250 pingCandidates);
1251 }
1252 if (pingNow) {
1253 for (final Account account : pingCandidates) {
1254 final var connection = account.getXmppConnection();
1255 final boolean lowTimeout = isInLowPingTimeoutMode(account);
1256 final var delta =
1257 (SystemClock.elapsedRealtime() - connection.getLastPacketReceived())
1258 / 1000L;
1259 connection.sendPing();
1260 Log.d(
1261 Config.LOGTAG,
1262 String.format(
1263 "%s: send ping (action=%s,lowTimeout=%s,interval=%s)",
1264 account.getJid().asBareJid(), action, lowTimeout, delta));
1265 scheduleWakeUpCall(
1266 lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT,
1267 account.getUuid().hashCode());
1268 }
1269 }
1270 long msToMucPing = (mLastMucPing + (Config.PING_MAX_INTERVAL * 2000L)) - SystemClock.elapsedRealtime();
1271 if (pingNow || ("ui".equals(action) && msToMucPing <= 0) || msToMucPing < -300000) {
1272 Log.d(Config.LOGTAG, "ping MUCs");
1273 mLastMucPing = SystemClock.elapsedRealtime();
1274 for (Conversation c : getConversations()) {
1275 if (c.getMode() == Conversation.MODE_MULTI && (c.getMucOptions().online() || c.getMucOptions().getError() == MucOptions.Error.SHUTDOWN)) {
1276 mucSelfPingAndRejoin(c);
1277 }
1278 }
1279 }
1280 WakeLockHelper.release(wakeLock);
1281 }
1282
1283 private void sendScheduledMessages() {
1284 Log.d(Config.LOGTAG, "looking for and sending scheduled messages");
1285
1286 for (final var message : new ArrayList<>(mScheduledMessages.values())) {
1287 if (message.getTimeSent() > System.currentTimeMillis()) continue;
1288
1289 final var conversation = message.getConversation();
1290 final var account = conversation.getAccount();
1291 final boolean inProgressJoin;
1292 synchronized (account.inProgressConferenceJoins) {
1293 inProgressJoin = account.inProgressConferenceJoins.contains(conversation);
1294 }
1295 final boolean pendingJoin;
1296 synchronized (account.pendingConferenceJoins) {
1297 pendingJoin = account.pendingConferenceJoins.contains(conversation);
1298 }
1299 if (conversation.getAccount() == account
1300 && !pendingJoin
1301 && !inProgressJoin) {
1302 resendMessage(message, false);
1303 }
1304 }
1305 }
1306
1307 private void handleOrbotStartedEvent() {
1308 for (final Account account : accounts) {
1309 if (account.getStatus() == Account.State.TOR_NOT_AVAILABLE) {
1310 reconnectAccount(account, true, false);
1311 }
1312 }
1313 }
1314
1315 private boolean processAccountState(
1316 final Account account,
1317 final boolean interactive,
1318 final boolean isUiAction,
1319 final boolean isAccountPushed,
1320 final HashSet<Account> pingCandidates) {
1321 if (!account.getStatus().isAttemptReconnect()) {
1322 return false;
1323 }
1324 final var requestCode = account.getUuid().hashCode();
1325 if (!hasInternetConnection()) {
1326 account.setStatus(Account.State.NO_INTERNET);
1327 statusListener.onStatusChanged(account);
1328 } else {
1329 if (account.getStatus() == Account.State.NO_INTERNET) {
1330 account.setStatus(Account.State.OFFLINE);
1331 statusListener.onStatusChanged(account);
1332 }
1333 if (account.getStatus() == Account.State.ONLINE) {
1334 synchronized (mLowPingTimeoutMode) {
1335 long lastReceived = account.getXmppConnection().getLastPacketReceived();
1336 long lastSent = account.getXmppConnection().getLastPingSent();
1337 long pingInterval =
1338 isUiAction
1339 ? Config.PING_MIN_INTERVAL * 1000
1340 : Config.PING_MAX_INTERVAL * 1000;
1341 long msToNextPing =
1342 (Math.max(lastReceived, lastSent) + pingInterval)
1343 - SystemClock.elapsedRealtime();
1344 int pingTimeout =
1345 mLowPingTimeoutMode.contains(account.getJid().asBareJid())
1346 ? Config.LOW_PING_TIMEOUT * 1000
1347 : Config.PING_TIMEOUT * 1000;
1348 long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
1349 if (lastSent > lastReceived) {
1350 if (pingTimeoutIn < 0) {
1351 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping timeout");
1352 this.reconnectAccount(account, true, interactive);
1353 } else {
1354 this.scheduleWakeUpCall(pingTimeoutIn, requestCode);
1355 }
1356 } else {
1357 pingCandidates.add(account);
1358 if (isAccountPushed) {
1359 if (mLowPingTimeoutMode.add(account.getJid().asBareJid())) {
1360 Log.d(
1361 Config.LOGTAG,
1362 account.getJid().asBareJid()
1363 + ": entering low ping timeout mode");
1364 }
1365 return true;
1366 } else if (msToNextPing <= 0) {
1367 return true;
1368 } else {
1369 this.scheduleWakeUpCall(msToNextPing, requestCode);
1370 if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
1371 Log.d(
1372 Config.LOGTAG,
1373 account.getJid().asBareJid()
1374 + ": leaving low ping timeout mode");
1375 }
1376 }
1377 }
1378 }
1379 } else if (account.getStatus() == Account.State.OFFLINE) {
1380 reconnectAccount(account, true, interactive);
1381 } else if (account.getStatus() == Account.State.CONNECTING) {
1382 final var connection = account.getXmppConnection();
1383 final var connectionDuration = connection.getConnectionDuration();
1384 final var discoDuration = connection.getDiscoDuration();
1385 final var connectionTimeout = Config.CONNECT_TIMEOUT * 1000L - connectionDuration;
1386 final var discoTimeout = Config.CONNECT_DISCO_TIMEOUT * 1000L - discoDuration;
1387 if (connectionTimeout < 0) {
1388 connection.triggerConnectionTimeout();
1389 } else if (discoTimeout < 0) {
1390 connection.sendDiscoTimeout();
1391 scheduleWakeUpCall(discoTimeout, requestCode);
1392 } else {
1393 scheduleWakeUpCall(Math.min(connectionTimeout, discoTimeout), requestCode);
1394 }
1395 } else {
1396 final boolean aggressive =
1397 account.getStatus() == Account.State.SEE_OTHER_HOST
1398 || hasJingleRtpConnection(account);
1399 if (account.getXmppConnection().getTimeToNextAttempt(aggressive) <= 0) {
1400 reconnectAccount(account, true, interactive);
1401 }
1402 }
1403 }
1404 return false;
1405 }
1406
1407 private void toggleSoftDisabled(final boolean softDisabled) {
1408 for (final Account account : this.accounts) {
1409 if (account.isEnabled()) {
1410 if (account.setOption(Account.OPTION_SOFT_DISABLED, softDisabled)) {
1411 updateAccount(account);
1412 }
1413 }
1414 }
1415 }
1416
1417 private void fetchServiceOutageStatus(final Account account) {
1418 final var sosUrl = account.getKey(Account.KEY_SOS_URL);
1419 if (Strings.isNullOrEmpty(sosUrl)) {
1420 return;
1421 }
1422 final var url = HttpUrl.parse(sosUrl);
1423 if (url == null) {
1424 return;
1425 }
1426 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching service outage " + url);
1427 Futures.addCallback(
1428 ServiceOutageStatus.fetch(getApplicationContext(), url),
1429 new FutureCallback<>() {
1430 @Override
1431 public void onSuccess(final ServiceOutageStatus sos) {
1432 Log.d(Config.LOGTAG, "fetched " + sos);
1433 account.setServiceOutageStatus(sos);
1434 updateAccountUi();
1435 }
1436
1437 @Override
1438 public void onFailure(@NonNull Throwable throwable) {
1439 Log.d(Config.LOGTAG, "error fetching sos", throwable);
1440 }
1441 },
1442 MoreExecutors.directExecutor());
1443 }
1444
1445 public boolean processUnifiedPushMessage(
1446 final Account account, final Jid transport, final Element push) {
1447 return unifiedPushBroker.processPushMessage(account, transport, push);
1448 }
1449
1450 public void reinitializeMuclumbusService() {
1451 mChannelDiscoveryService.initializeMuclumbusService();
1452 }
1453
1454 public void discoverChannels(
1455 String query,
1456 ChannelDiscoveryService.Method method,
1457 Map<Jid, XmppConnection> mucServices,
1458 ChannelDiscoveryService.OnChannelSearchResultsFound onChannelSearchResultsFound) {
1459 mChannelDiscoveryService.discover(
1460 Strings.nullToEmpty(query).trim(), method, mucServices, onChannelSearchResultsFound);
1461 }
1462
1463 public boolean isDataSaverDisabled() {
1464 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
1465 return true;
1466 }
1467 final ConnectivityManager connectivityManager = getSystemService(ConnectivityManager.class);
1468 return !Compatibility.isActiveNetworkMetered(connectivityManager)
1469 || Compatibility.getRestrictBackgroundStatus(connectivityManager)
1470 == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
1471 }
1472
1473 private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {
1474 final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid);
1475 Message message = new Message(conversation, body, conversation.getNextEncryption());
1476 if (inReplyTo != null) {
1477 if (Emoticons.isEmoji(body.replaceAll("\\s", ""))) {
1478 final var aggregated = inReplyTo.getAggregatedReactions();
1479 final ImmutableSet.Builder<String> reactionBuilder = new ImmutableSet.Builder<>();
1480 reactionBuilder.addAll(aggregated.ourReactions);
1481 reactionBuilder.add(body.replaceAll("\\s", ""));
1482 sendReactions(inReplyTo, reactionBuilder.build());
1483 return;
1484 } else {
1485 message = inReplyTo.reply();
1486 }
1487 message.clearFallbacks("urn:xmpp:reply:0");
1488 message.setBody(body);
1489 message.setEncryption(conversation.getNextEncryption());
1490 }
1491 if (inReplyTo != null && inReplyTo.isPrivateMessage()) {
1492 Message.configurePrivateMessage(message, inReplyTo.getCounterpart());
1493 }
1494 message.markUnread();
1495 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1496 getPgpEngine()
1497 .encrypt(
1498 message,
1499 new UiCallback<Message>() {
1500 @Override
1501 public void success(Message message) {
1502 if (dismissAfterReply) {
1503 markRead((Conversation) message.getConversation(), true);
1504 } else {
1505 mNotificationService.pushFromDirectReply(message);
1506 }
1507 }
1508
1509 @Override
1510 public void error(int errorCode, Message object) {}
1511
1512 @Override
1513 public void userInputRequired(PendingIntent pi, Message object) {}
1514 });
1515 } else {
1516 sendMessage(message);
1517 if (dismissAfterReply) {
1518 markRead(conversation, true);
1519 } else {
1520 mNotificationService.pushFromDirectReply(message);
1521 }
1522 }
1523 }
1524
1525 private boolean dndOnSilentMode() {
1526 return getBooleanPreference(AppSettings.DND_ON_SILENT_MODE, R.bool.dnd_on_silent_mode);
1527 }
1528
1529 private boolean manuallyChangePresence() {
1530 return getBooleanPreference(
1531 AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
1532 }
1533
1534 private boolean treatVibrateAsSilent() {
1535 return getBooleanPreference(
1536 AppSettings.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent);
1537 }
1538
1539 private boolean awayWhenScreenLocked() {
1540 return getBooleanPreference(
1541 AppSettings.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off);
1542 }
1543
1544 private String getCompressPicturesPreference() {
1545 return getPreferences()
1546 .getString(
1547 "picture_compression",
1548 getResources().getString(R.string.picture_compression));
1549 }
1550
1551 private im.conversations.android.xmpp.model.stanza.Presence.Availability getTargetPresence() {
1552 if (dndOnSilentMode() && isPhoneSilenced()) {
1553 return im.conversations.android.xmpp.model.stanza.Presence.Availability.DND;
1554 } else if (awayWhenScreenLocked() && isScreenLocked()) {
1555 return im.conversations.android.xmpp.model.stanza.Presence.Availability.AWAY;
1556 } else {
1557 return im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE;
1558 }
1559 }
1560
1561 public boolean isScreenLocked() {
1562 final KeyguardManager keyguardManager = getSystemService(KeyguardManager.class);
1563 final PowerManager powerManager = getSystemService(PowerManager.class);
1564 final boolean locked = keyguardManager != null && keyguardManager.isKeyguardLocked();
1565 final boolean interactive;
1566 try {
1567 interactive = powerManager != null && powerManager.isInteractive();
1568 } catch (final Exception e) {
1569 return false;
1570 }
1571 return locked || !interactive;
1572 }
1573
1574 private boolean isPhoneSilenced() {
1575 final NotificationManager notificationManager = getSystemService(NotificationManager.class);
1576 final int filter =
1577 notificationManager == null
1578 ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN
1579 : notificationManager.getCurrentInterruptionFilter();
1580 final boolean notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY;
1581 final AudioManager audioManager = getSystemService(AudioManager.class);
1582 final int ringerMode =
1583 audioManager == null
1584 ? AudioManager.RINGER_MODE_NORMAL
1585 : audioManager.getRingerMode();
1586 try {
1587 if (treatVibrateAsSilent()) {
1588 return notificationDnd || ringerMode != AudioManager.RINGER_MODE_NORMAL;
1589 } else {
1590 return notificationDnd || ringerMode == AudioManager.RINGER_MODE_SILENT;
1591 }
1592 } catch (final Throwable throwable) {
1593 Log.d(
1594 Config.LOGTAG,
1595 "platform bug in isPhoneSilenced (" + throwable.getMessage() + ")");
1596 return notificationDnd;
1597 }
1598 }
1599
1600 private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) {
1601 Log.d(Config.LOGTAG, "resetting all attempt counts");
1602 for (Account account : accounts) {
1603 if (account.hasErrorStatus() || reallyAll) {
1604 final XmppConnection connection = account.getXmppConnection();
1605 if (connection != null) {
1606 connection.resetAttemptCount(retryImmediately);
1607 }
1608 }
1609 if (account.setShowErrorNotification(true)) {
1610 mDatabaseWriterExecutor.execute(() -> databaseBackend.updateAccount(account));
1611 }
1612 }
1613 mNotificationService.updateErrorNotification();
1614 }
1615
1616 private void dismissErrorNotifications() {
1617 for (final Account account : this.accounts) {
1618 if (account.hasErrorStatus()) {
1619 Log.d(
1620 Config.LOGTAG,
1621 account.getJid().asBareJid() + ": dismissing error notification");
1622 if (account.setShowErrorNotification(false)) {
1623 mDatabaseWriterExecutor.execute(() -> databaseBackend.updateAccount(account));
1624 }
1625 }
1626 }
1627 }
1628
1629 private void expireOldMessages() {
1630 expireOldMessages(false);
1631 }
1632
1633 public void expireOldMessages(final boolean resetHasMessagesLeftOnServer) {
1634 mLastExpiryRun.set(SystemClock.elapsedRealtime());
1635 mDatabaseWriterExecutor.execute(
1636 () -> {
1637 long timestamp = getAutomaticMessageDeletionDate();
1638 if (timestamp > 0) {
1639 databaseBackend.expireOldMessages(timestamp);
1640 synchronized (XmppConnectionService.this.conversations) {
1641 for (Conversation conversation :
1642 XmppConnectionService.this.conversations) {
1643 conversation.expireOldMessages(timestamp);
1644 if (resetHasMessagesLeftOnServer) {
1645 conversation.messagesLoaded.set(true);
1646 conversation.setHasMessagesLeftOnServer(true);
1647 }
1648 }
1649 }
1650 updateConversationUi();
1651 }
1652 });
1653 }
1654
1655 public boolean hasInternetConnection() {
1656 final ConnectivityManager cm =
1657 ContextCompat.getSystemService(this, ConnectivityManager.class);
1658 if (cm == null) {
1659 return true; // if internet connection can not be checked it is probably best to just
1660 // try
1661 }
1662 try {
1663 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
1664 final Network activeNetwork = cm.getActiveNetwork();
1665 final NetworkCapabilities capabilities =
1666 activeNetwork == null ? null : cm.getNetworkCapabilities(activeNetwork);
1667 return capabilities != null
1668 && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
1669 } else {
1670 final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
1671 return networkInfo != null
1672 && (networkInfo.isConnected()
1673 || networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET);
1674 }
1675 } catch (final RuntimeException e) {
1676 Log.d(Config.LOGTAG, "unable to check for internet connection", e);
1677 return true; // if internet connection can not be checked it is probably best to just
1678 // try
1679 }
1680 }
1681
1682 @SuppressLint("TrulyRandom")
1683 @Override
1684 public void onCreate() {
1685 com.cheogram.android.AndroidLoggingHandler.reset(new com.cheogram.android.AndroidLoggingHandler());
1686 java.util.logging.Logger.getLogger("").setLevel(java.util.logging.Level.FINEST);
1687 LibIdnXmppStringprep.setup();
1688 emojiSearch = new EmojiSearch(this);
1689 setTheme(R.style.Theme_Conversations3);
1690 ThemeHelper.applyCustomColors(this);
1691 if (Compatibility.twentySix()) {
1692 mNotificationService.initializeChannels();
1693 }
1694 mChannelDiscoveryService.initializeMuclumbusService();
1695 mForceDuringOnCreate.set(Compatibility.twentySix());
1696 toggleForegroundService();
1697 this.destroyed = false;
1698 OmemoSetting.load(this);
1699 try {
1700 Security.insertProviderAt(Conscrypt.newProvider(), 1);
1701 } catch (Throwable throwable) {
1702 Log.e(Config.LOGTAG, "unable to initialize security provider", throwable);
1703 }
1704 Resolver.init(this);
1705 updateMemorizingTrustManager();
1706 final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
1707 final int cacheSize = maxMemory / 8;
1708 this.mDrawableCache = new LruCache<String, Drawable>(cacheSize) {
1709 @Override
1710 protected int sizeOf(final String key, final Drawable drawable) {
1711 if (drawable instanceof BitmapDrawable) {
1712 Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
1713 if (bitmap == null) return 1024;
1714
1715 return bitmap.getByteCount() / 1024;
1716 } else if (drawable instanceof AvatarService.TextDrawable) {
1717 return 50;
1718 } else {
1719 return drawable.getIntrinsicWidth() * drawable.getIntrinsicHeight() * 40 / 1024;
1720 }
1721 }
1722 };
1723 if (mLastActivity == 0) {
1724 mLastActivity =
1725 getPreferences().getLong(SETTING_LAST_ACTIVITY_TS, System.currentTimeMillis());
1726 }
1727
1728 Log.d(Config.LOGTAG, "initializing database...");
1729 this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
1730 Log.d(Config.LOGTAG, "restoring accounts...");
1731 this.accounts = databaseBackend.getAccounts();
1732 for (Account account : this.accounts) {
1733 final int color = getPreferences().getInt("account_color:" + account.getUuid(), 0);
1734 if (color != 0) account.setColor(color);
1735 }
1736 final SharedPreferences.Editor editor = getPreferences().edit();
1737 final boolean hasEnabledAccounts = hasEnabledAccounts();
1738 editor.putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
1739 editor.apply();
1740 toggleSetProfilePictureActivity(hasEnabledAccounts);
1741 reconfigurePushDistributor();
1742
1743 if (CallIntegration.hasSystemFeature(this)) {
1744 CallIntegrationConnectionService.togglePhoneAccountsAsync(this, this.accounts);
1745 }
1746
1747 restoreFromDatabase();
1748
1749 if (QuickConversationsService.isContactListIntegration(this)
1750 && ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
1751 == PackageManager.PERMISSION_GRANTED) {
1752 startContactObserver();
1753 }
1754 FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath);
1755 if (Compatibility.hasStoragePermission(this)) {
1756 Log.d(Config.LOGTAG, "starting file observer");
1757 FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::startWatching);
1758 FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
1759 }
1760 if (Config.supportOpenPgp()) {
1761 this.pgpServiceConnection =
1762 new OpenPgpServiceConnection(
1763 this,
1764 "org.sufficientlysecure.keychain",
1765 new OpenPgpServiceConnection.OnBound() {
1766 @Override
1767 public void onBound(final IOpenPgpService2 service) {
1768 for (Account account : accounts) {
1769 final PgpDecryptionService pgp =
1770 account.getPgpDecryptionService();
1771 if (pgp != null) {
1772 pgp.continueDecryption(true);
1773 }
1774 }
1775 }
1776
1777 @Override
1778 public void onError(final Exception exception) {
1779 Log.e(
1780 Config.LOGTAG,
1781 "could not bind to OpenKeyChain",
1782 exception);
1783 }
1784 });
1785 this.pgpServiceConnection.bindToService();
1786 }
1787
1788 final PowerManager powerManager = getSystemService(PowerManager.class);
1789 if (powerManager != null) {
1790 this.wakeLock =
1791 powerManager.newWakeLock(
1792 PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service");
1793 }
1794
1795 toggleForegroundService();
1796 updateUnreadCountBadge();
1797 toggleScreenEventReceiver();
1798 final IntentFilter systemBroadcastFilter = new IntentFilter();
1799 scheduleNextIdlePing();
1800 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1801 systemBroadcastFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
1802 }
1803 systemBroadcastFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
1804 ContextCompat.registerReceiver(
1805 this,
1806 this.mInternalEventReceiver,
1807 systemBroadcastFilter,
1808 ContextCompat.RECEIVER_NOT_EXPORTED);
1809 final IntentFilter exportedBroadcastFilter = new IntentFilter();
1810 exportedBroadcastFilter.addAction(TorServiceUtils.ACTION_STATUS);
1811 ContextCompat.registerReceiver(
1812 this,
1813 this.mInternalRestrictedEventReceiver,
1814 exportedBroadcastFilter,
1815 ContextCompat.RECEIVER_EXPORTED);
1816 mForceDuringOnCreate.set(false);
1817 toggleForegroundService();
1818 rescanStickers();
1819 cleanupCache();
1820 internalPingExecutor.scheduleWithFixedDelay(
1821 this::manageAccountConnectionStatesInternal, 10, 10, TimeUnit.SECONDS);
1822 final SharedPreferences sharedPreferences =
1823 androidx.preference.PreferenceManager.getDefaultSharedPreferences(this);
1824 sharedPreferences.registerOnSharedPreferenceChangeListener(
1825 new SharedPreferences.OnSharedPreferenceChangeListener() {
1826 @Override
1827 public void onSharedPreferenceChanged(
1828 SharedPreferences sharedPreferences, @Nullable String key) {
1829 Log.d(Config.LOGTAG, "preference '" + key + "' has changed");
1830 if (AppSettings.KEEP_FOREGROUND_SERVICE.equals(key)) {
1831 toggleForegroundService();
1832 }
1833 }
1834 });
1835 }
1836
1837 private void checkForDeletedFiles() {
1838 if (destroyed) {
1839 Log.d(
1840 Config.LOGTAG,
1841 "Do not check for deleted files because service has been destroyed");
1842 return;
1843 }
1844 final long start = SystemClock.elapsedRealtime();
1845 final List<DatabaseBackend.FilePathInfo> relativeFilePaths =
1846 databaseBackend.getFilePathInfo();
1847 final List<DatabaseBackend.FilePathInfo> changed = new ArrayList<>();
1848 for (final DatabaseBackend.FilePathInfo filePath : relativeFilePaths) {
1849 if (destroyed) {
1850 Log.d(
1851 Config.LOGTAG,
1852 "Stop checking for deleted files because service has been destroyed");
1853 return;
1854 }
1855 final File file = fileBackend.getFileForPath(filePath.path);
1856 if (filePath.setDeleted(!file.exists())) {
1857 changed.add(filePath);
1858 }
1859 }
1860 final long duration = SystemClock.elapsedRealtime() - start;
1861 Log.d(
1862 Config.LOGTAG,
1863 "found "
1864 + changed.size()
1865 + " changed files on start up. total="
1866 + relativeFilePaths.size()
1867 + ". ("
1868 + duration
1869 + "ms)");
1870 if (changed.size() > 0) {
1871 databaseBackend.markFilesAsChanged(changed);
1872 markChangedFiles(changed);
1873 }
1874 }
1875
1876 public void startContactObserver() {
1877 getContentResolver()
1878 .registerContentObserver(
1879 ContactsContract.Contacts.CONTENT_URI,
1880 true,
1881 new ContentObserver(null) {
1882 @Override
1883 public void onChange(boolean selfChange) {
1884 super.onChange(selfChange);
1885 if (restoredFromDatabaseLatch.getCount() == 0) {
1886 loadPhoneContacts();
1887 }
1888 }
1889 });
1890 }
1891
1892 @Override
1893 public void onTrimMemory(int level) {
1894 super.onTrimMemory(level);
1895 if (level >= TRIM_MEMORY_COMPLETE) {
1896 Log.d(Config.LOGTAG, "clear cache due to low memory");
1897 getDrawableCache().evictAll();
1898 }
1899 }
1900
1901 @Override
1902 public void onDestroy() {
1903 try {
1904 unregisterReceiver(this.mInternalEventReceiver);
1905 unregisterReceiver(this.mInternalRestrictedEventReceiver);
1906 unregisterReceiver(this.mInternalScreenEventReceiver);
1907 } catch (final IllegalArgumentException e) {
1908 // ignored
1909 }
1910 destroyed = false;
1911 fileObserver.stopWatching();
1912 internalPingExecutor.shutdown();
1913 super.onDestroy();
1914 }
1915
1916 public void restartFileObserver() {
1917 Log.d(Config.LOGTAG, "restarting file observer");
1918 FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::restartWatching);
1919 FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
1920 }
1921
1922 public void toggleScreenEventReceiver() {
1923 if (awayWhenScreenLocked() && !manuallyChangePresence()) {
1924 final IntentFilter filter = new IntentFilter();
1925 filter.addAction(Intent.ACTION_SCREEN_ON);
1926 filter.addAction(Intent.ACTION_SCREEN_OFF);
1927 filter.addAction(Intent.ACTION_USER_PRESENT);
1928 registerReceiver(this.mInternalScreenEventReceiver, filter);
1929 } else {
1930 try {
1931 unregisterReceiver(this.mInternalScreenEventReceiver);
1932 } catch (IllegalArgumentException e) {
1933 // ignored
1934 }
1935 }
1936 }
1937
1938 public void toggleForegroundService() {
1939 toggleForegroundService(false, false);
1940 }
1941
1942 public void setOngoingCall(
1943 AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
1944 ongoingCall.set(new OngoingCall(id, media, reconnecting));
1945 toggleForegroundService(false, true);
1946 }
1947
1948 public void removeOngoingCall() {
1949 ongoingCall.set(null);
1950 toggleForegroundService(false, false);
1951 }
1952
1953 private void toggleForegroundService(boolean force, boolean needMic) {
1954 final boolean status;
1955 final OngoingCall ongoing = ongoingCall.get();
1956 final boolean ongoingVideoTranscoding = mOngoingVideoTranscoding.get();
1957 final int id;
1958 if (force
1959 || mForceDuringOnCreate.get()
1960 || ongoingVideoTranscoding
1961 || ongoing != null
1962 || (appSettings.isKeepForegroundService() && hasEnabledAccounts())) {
1963 final Notification notification;
1964 if (ongoing != null && !diallerIntegrationActive.get()) {
1965 notification = this.mNotificationService.getOngoingCallNotification(ongoing);
1966 id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
1967 startForegroundOrCatch(id, notification, true);
1968 } else if (ongoingVideoTranscoding) {
1969 notification = this.mNotificationService.getIndeterminateVideoTranscoding();
1970 id = NotificationService.ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID;
1971 startForegroundOrCatch(id, notification, false);
1972 } else {
1973 notification = this.mNotificationService.createForegroundNotification();
1974 id = NotificationService.FOREGROUND_NOTIFICATION_ID;
1975 startForegroundOrCatch(id, notification, needMic || ongoing != null || diallerIntegrationActive.get());
1976 }
1977 mNotificationService.notify(id, notification);
1978 status = true;
1979 } else {
1980 id = 0;
1981 stopForeground(true);
1982 status = false;
1983 }
1984
1985 for (final int toBeRemoved :
1986 Collections2.filter(
1987 Arrays.asList(
1988 NotificationService.FOREGROUND_NOTIFICATION_ID,
1989 NotificationService.ONGOING_CALL_NOTIFICATION_ID,
1990 NotificationService.ONGOING_VIDEO_TRANSCODING_NOTIFICATION_ID),
1991 i -> i != id)) {
1992 mNotificationService.cancel(toBeRemoved);
1993 }
1994 Log.d(
1995 Config.LOGTAG,
1996 "ForegroundService: " + (status ? "on" : "off") + ", notification: " + id);
1997 }
1998
1999 private void startForegroundOrCatch(
2000 final int id, final Notification notification, final boolean requireMicrophone) {
2001 try {
2002 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
2003 final int foregroundServiceType;
2004 if (requireMicrophone
2005 && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
2006 == PackageManager.PERMISSION_GRANTED) {
2007 foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
2008 Log.d(Config.LOGTAG, "defaulting to microphone foreground service type");
2009 } else if (getSystemService(PowerManager.class)
2010 .isIgnoringBatteryOptimizations(getPackageName())) {
2011 foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED;
2012 } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
2013 == PackageManager.PERMISSION_GRANTED) {
2014 foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE;
2015 } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
2016 == PackageManager.PERMISSION_GRANTED) {
2017 foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA;
2018 } else {
2019 foregroundServiceType = ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE;
2020 Log.w(Config.LOGTAG, "falling back to special use foreground service type");
2021 }
2022
2023 startForeground(id, notification, foregroundServiceType);
2024 } else {
2025 startForeground(id, notification);
2026 }
2027 } catch (final IllegalStateException | SecurityException e) {
2028 Log.e(Config.LOGTAG, "Could not start foreground service", e);
2029 }
2030 }
2031
2032 public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() {
2033 return !mOngoingVideoTranscoding.get()
2034 && ongoingCall.get() == null
2035 && appSettings.isKeepForegroundService()
2036 && hasEnabledAccounts();
2037 }
2038
2039 @Override
2040 public void onTaskRemoved(final Intent rootIntent) {
2041 super.onTaskRemoved(rootIntent);
2042 if ((appSettings.isKeepForegroundService() && hasEnabledAccounts())
2043 || mOngoingVideoTranscoding.get()
2044 || ongoingCall.get() != null) {
2045 Log.d(Config.LOGTAG, "ignoring onTaskRemoved because foreground service is activated");
2046 } else {
2047 this.logoutAndSave(false);
2048 }
2049 }
2050
2051 private void logoutAndSave(boolean stop) {
2052 int activeAccounts = 0;
2053 for (final Account account : accounts) {
2054 if (account.isConnectionEnabled()) {
2055 databaseBackend.writeRoster(account.getRoster());
2056 activeAccounts++;
2057 }
2058 if (account.getXmppConnection() != null) {
2059 new Thread(() -> disconnect(account, false)).start();
2060 }
2061 }
2062 if (stop || activeAccounts == 0) {
2063 Log.d(Config.LOGTAG, "good bye");
2064 stopSelf();
2065 }
2066 }
2067
2068 private void schedulePostConnectivityChange() {
2069 final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
2070 if (alarmManager == null) {
2071 return;
2072 }
2073 final long triggerAtMillis =
2074 SystemClock.elapsedRealtime()
2075 + (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL * 1000);
2076 final Intent intent = new Intent(this, SystemEventReceiver.class);
2077 intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE);
2078 try {
2079 final PendingIntent pendingIntent =
2080 PendingIntent.getBroadcast(
2081 this,
2082 1,
2083 intent,
2084 s()
2085 ? PendingIntent.FLAG_IMMUTABLE
2086 | PendingIntent.FLAG_UPDATE_CURRENT
2087 : PendingIntent.FLAG_UPDATE_CURRENT);
2088 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
2089 alarmManager.setAndAllowWhileIdle(
2090 AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
2091 } else {
2092 alarmManager.set(
2093 AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
2094 }
2095 } catch (RuntimeException e) {
2096 Log.e(Config.LOGTAG, "unable to schedule alarm for post connectivity change", e);
2097 }
2098 }
2099
2100 public void scheduleWakeUpCall(final int seconds, final int requestCode) {
2101 scheduleWakeUpCall((seconds < 0 ? 1 : seconds + 1) * 1000L, requestCode);
2102 }
2103
2104 public void scheduleWakeUpCall(final long milliSeconds, final int requestCode) {
2105 final var timeToWake = SystemClock.elapsedRealtime() + milliSeconds;
2106 final var alarmManager = getSystemService(AlarmManager.class);
2107 final Intent intent = new Intent(this, SystemEventReceiver.class);
2108 intent.setAction(ACTION_PING);
2109 try {
2110 final PendingIntent pendingIntent =
2111 PendingIntent.getBroadcast(
2112 this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE);
2113 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
2114 } catch (final RuntimeException e) {
2115 Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e);
2116 }
2117 }
2118
2119 private void scheduleNextIdlePing() {
2120 long timeUntilWake = Config.IDLE_PING_INTERVAL * 1000;
2121 final var now = System.currentTimeMillis();
2122 for (final var message : mScheduledMessages.values()) {
2123 if (message.getTimeSent() <= now) continue; // Just in case
2124 if (message.getTimeSent() - now < timeUntilWake) timeUntilWake = message.getTimeSent() - now;
2125 }
2126 final var timeToWake = SystemClock.elapsedRealtime() + timeUntilWake;
2127 final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
2128 if (alarmManager == null) {
2129 Log.d(Config.LOGTAG, "no alarm manager?");
2130 return;
2131 }
2132 final Intent intent = new Intent(this, SystemEventReceiver.class);
2133 intent.setAction(ACTION_IDLE_PING);
2134 try {
2135 final PendingIntent pendingIntent =
2136 PendingIntent.getBroadcast(
2137 this,
2138 0,
2139 intent,
2140 s()
2141 ? PendingIntent.FLAG_IMMUTABLE
2142 | PendingIntent.FLAG_UPDATE_CURRENT
2143 : PendingIntent.FLAG_UPDATE_CURRENT);
2144 alarmManager.setAndAllowWhileIdle(
2145 AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
2146 } catch (RuntimeException e) {
2147 Log.d(Config.LOGTAG, "unable to schedule alarm for idle ping", e);
2148 }
2149 }
2150
2151 public XmppConnection createConnection(final Account account) {
2152 final XmppConnection connection = new XmppConnection(account, this);
2153 connection.setOnStatusChangedListener(this.statusListener);
2154 connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket));
2155 connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
2156 connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
2157 connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService);
2158 AxolotlService axolotlService = account.getAxolotlService();
2159 if (axolotlService != null) {
2160 connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
2161 }
2162 return connection;
2163 }
2164
2165 public void sendChatState(Conversation conversation) {
2166 if (sendChatStates()) {
2167 final var packet = mMessageGenerator.generateChatState(conversation);
2168 sendMessagePacket(conversation.getAccount(), packet);
2169 }
2170 }
2171
2172 private void sendFileMessage(
2173 final Message message, final boolean delay, final boolean forceP2P, final Runnable cb) {
2174 final var account = message.getConversation().getAccount();
2175 Log.d(
2176 Config.LOGTAG,
2177 account.getJid().asBareJid() + ": send file message. forceP2P=" + forceP2P);
2178 if ((account.httpUploadAvailable(fileBackend.getFile(message, false).getSize())
2179 || message.getConversation().getMode() == Conversation.MODE_MULTI)
2180 && !forceP2P) {
2181 mHttpConnectionManager.createNewUploadConnection(message, delay, cb);
2182 } else {
2183 mJingleConnectionManager.startJingleFileTransfer(message);
2184 if (cb != null) cb.run();
2185 }
2186 }
2187
2188 public void sendMessage(final Message message) {
2189 sendMessage(message, false, false, false, false, null);
2190 }
2191
2192 public void sendMessage(final Message message, final Runnable cb) {
2193 sendMessage(message, false, false, false, false, cb);
2194 }
2195
2196 private void sendMessage(final Message message, final boolean resend, final boolean previewedLinks, final boolean delay, final Runnable cb) {
2197 sendMessage(message, resend, previewedLinks, delay, false, cb);
2198 }
2199
2200 private void sendMessage(
2201 final Message message,
2202 final boolean resend,
2203 final boolean previewedLinks,
2204 final boolean delay,
2205 final boolean forceP2P,
2206 final Runnable cb) {
2207 final Account account = message.getConversation().getAccount();
2208 if (account.setShowErrorNotification(true)) {
2209 databaseBackend.updateAccount(account);
2210 mNotificationService.updateErrorNotification();
2211 }
2212 final Conversation conversation = (Conversation) message.getConversation();
2213 account.deactivateGracePeriod();
2214
2215 if (QuickConversationsService.isQuicksy()
2216 && conversation.getMode() == Conversation.MODE_SINGLE) {
2217 final Contact contact = conversation.getContact();
2218 if (!contact.showInRoster() && contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) {
2219 Log.d(
2220 Config.LOGTAG,
2221 account.getJid().asBareJid()
2222 + ": adding "
2223 + contact.getJid()
2224 + " on sending message");
2225 createContact(contact, true);
2226 }
2227 }
2228
2229 im.conversations.android.xmpp.model.stanza.Message packet = null;
2230 final boolean addToConversation = !message.edited() && message.getRawBody() != null;
2231 boolean saveInDb = addToConversation;
2232 message.setStatus(Message.STATUS_WAITING);
2233
2234 if (message.getEncryption() != Message.ENCRYPTION_NONE
2235 && conversation.getMode() == Conversation.MODE_MULTI
2236 && conversation.isPrivateAndNonAnonymous()) {
2237 if (conversation.setAttribute(
2238 Conversation.ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, true)) {
2239 databaseBackend.updateConversation(conversation);
2240 }
2241 }
2242
2243 final boolean inProgressJoin = isJoinInProgress(conversation);
2244
2245 if (message.getCounterpart() == null && !message.isPrivateMessage()) {
2246 message.setCounterpart(message.getConversation().getJid().asBareJid());
2247 }
2248
2249 boolean waitForPreview = false;
2250 if (getPreferences().getBoolean("send_link_previews", true) && !previewedLinks && !message.needsUploading() && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
2251 message.clearLinkDescriptions();
2252 final List<URI> links = message.getLinks();
2253 if (!links.isEmpty()) {
2254 waitForPreview = true;
2255 if (account.isOnlineAndConnected()) {
2256 FILE_ATTACHMENT_EXECUTOR.execute(() -> {
2257 for (URI link : links) {
2258 if ("https".equals(link.getScheme())) {
2259 try {
2260 HttpUrl url = HttpUrl.parse(link.toString());
2261 OkHttpClient http = getHttpConnectionManager().buildHttpClient(url, account, 5, false);
2262 final var request = new okhttp3.Request.Builder().url(url).head().build();
2263 okhttp3.Response response = null;
2264 if ("www.amazon.com".equals(link.getHost()) || "www.amazon.ca".equals(link.getHost())) {
2265 // Amazon blocks HEAD
2266 response = new okhttp3.Response.Builder().request(request).protocol(okhttp3.Protocol.HTTP_1_1).code(200).message("OK").addHeader("Content-Type", "text/html").build();
2267 } else {
2268 response = http.newCall(request).execute();
2269 }
2270 final String mimeType = response.header("Content-Type") == null ? "" : response.header("Content-Type");
2271 final boolean image = mimeType.startsWith("image/");
2272 final boolean audio = mimeType.startsWith("audio/");
2273 final boolean video = mimeType.startsWith("video/");
2274 final boolean pdf = mimeType.equals("application/pdf");
2275 final boolean html = mimeType.startsWith("text/html") || mimeType.startsWith("application/xhtml+xml");
2276 if (response.isSuccessful() && (image || audio || video || pdf)) {
2277 Message.FileParams params = message.getFileParams();
2278 params.url = url.toString();
2279 if (response.header("Content-Length") != null) params.size = Long.parseLong(response.header("Content-Length"), 10);
2280 if (!Message.configurePrivateFileMessage(message)) {
2281 message.setType(image ? Message.TYPE_IMAGE : Message.TYPE_FILE);
2282 }
2283 params.setName(HttpConnectionManager.extractFilenameFromResponse(response));
2284
2285 if (link.toString().equals(message.getRawBody())) {
2286 Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
2287 fallback.addChild("body", "urn:xmpp:fallback:0");
2288 message.addPayload(fallback);
2289 } else if (message.getRawBody().indexOf(link.toString()) >= 0) {
2290 // Part of the real body, not just a fallback
2291 Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
2292 fallback.addChild("body", "urn:xmpp:fallback:0")
2293 .setAttribute("start", "0")
2294 .setAttribute("end", "0");
2295 message.addPayload(fallback);
2296 }
2297
2298 final int encryption = message.getEncryption();
2299 getHttpConnectionManager().createNewDownloadConnection(message, false, (file) -> {
2300 message.setEncryption(encryption);
2301 synchronized (message.getConversation()) {
2302 if (message.getStatus() == Message.STATUS_WAITING) sendMessage(message, true, true, false, cb);
2303 }
2304 });
2305 return;
2306 } else if (response.isSuccessful() && html) {
2307 Semaphore waiter = new Semaphore(0);
2308 OpenGraphParser.Builder openGraphBuilder = new OpenGraphParser.Builder(new OpenGraphCallback() {
2309 @Override
2310 public void onPostResponse(OpenGraphResult result) {
2311 Element rdf = new Element("Description", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
2312 rdf.setAttribute("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
2313 rdf.setAttribute("rdf:about", link.toString());
2314 if (result.getTitle() != null && !"".equals(result.getTitle())) {
2315 rdf.addChild("title", "https://ogp.me/ns#").setContent(result.getTitle());
2316 }
2317 if (result.getDescription() != null && !"".equals(result.getDescription())) {
2318 rdf.addChild("description", "https://ogp.me/ns#").setContent(result.getDescription());
2319 }
2320 if (result.getUrl() != null) {
2321 rdf.addChild("url", "https://ogp.me/ns#").setContent(result.getUrl());
2322 }
2323 if (result.getImage() != null) {
2324 rdf.addChild("image", "https://ogp.me/ns#").setContent(result.getImage());
2325 }
2326 if (result.getType() != null) {
2327 rdf.addChild("type", "https://ogp.me/ns#").setContent(result.getType());
2328 }
2329 if (result.getSiteName() != null) {
2330 rdf.addChild("site_name", "https://ogp.me/ns#").setContent(result.getSiteName());
2331 }
2332 if (result.getVideo() != null) {
2333 rdf.addChild("video", "https://ogp.me/ns#").setContent(result.getVideo());
2334 }
2335 message.addPayload(rdf);
2336 waiter.release();
2337 }
2338
2339 public void onError(String error) {
2340 waiter.release();
2341 }
2342 })
2343 .showNullOnEmpty(true)
2344 .maxBodySize(90000)
2345 .timeout(5000);
2346 if (useTorToConnect()) {
2347 openGraphBuilder = openGraphBuilder.jsoupProxy(new JsoupProxy("127.0.0.1", 8118));
2348 }
2349 openGraphBuilder.build().parse(link.toString());
2350 waiter.tryAcquire(10L, TimeUnit.SECONDS);
2351 }
2352 } catch (final IOException | InterruptedException e) { }
2353 }
2354 }
2355 synchronized (message.getConversation()) {
2356 if (message.getStatus() == Message.STATUS_WAITING) sendMessage(message, true, true, false, cb);
2357 }
2358 });
2359 }
2360 }
2361 }
2362
2363 boolean passedCbOn = false;
2364 if (account.isOnlineAndConnected() && !inProgressJoin && !waitForPreview && message.getTimeSent() <= System.currentTimeMillis()) {
2365 switch (message.getEncryption()) {
2366 case Message.ENCRYPTION_NONE:
2367 if (message.needsUploading()) {
2368 if (account.httpUploadAvailable(
2369 fileBackend.getFile(message, false).getSize())
2370 || conversation.getMode() == Conversation.MODE_MULTI
2371 || message.fixCounterpart()) {
2372 this.sendFileMessage(message, delay, forceP2P, cb);
2373 passedCbOn = true;
2374 } else {
2375 break;
2376 }
2377 } else {
2378 packet = mMessageGenerator.generateChat(message);
2379 }
2380 break;
2381 case Message.ENCRYPTION_PGP:
2382 case Message.ENCRYPTION_DECRYPTED:
2383 if (message.needsUploading()) {
2384 if (account.httpUploadAvailable(
2385 fileBackend.getFile(message, false).getSize())
2386 || conversation.getMode() == Conversation.MODE_MULTI
2387 || message.fixCounterpart()) {
2388 this.sendFileMessage(message, delay, forceP2P, cb);
2389 passedCbOn = true;
2390 } else {
2391 break;
2392 }
2393 } else {
2394 packet = mMessageGenerator.generatePgpChat(message);
2395 }
2396 break;
2397 case Message.ENCRYPTION_AXOLOTL:
2398 message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
2399 if (message.needsUploading()) {
2400 if (account.httpUploadAvailable(
2401 fileBackend.getFile(message, false).getSize())
2402 || conversation.getMode() == Conversation.MODE_MULTI
2403 || message.fixCounterpart()) {
2404 this.sendFileMessage(message, delay, forceP2P, cb);
2405 passedCbOn = true;
2406 } else {
2407 break;
2408 }
2409 } else {
2410 XmppAxolotlMessage axolotlMessage =
2411 account.getAxolotlService().fetchAxolotlMessageFromCache(message);
2412 if (axolotlMessage == null) {
2413 account.getAxolotlService().preparePayloadMessage(message, delay);
2414 } else {
2415 packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
2416 }
2417 }
2418 break;
2419 }
2420 if (packet != null) {
2421 if (account.getXmppConnection().getFeatures().sm()
2422 || (conversation.getMode() == Conversation.MODE_MULTI
2423 && message.getCounterpart().isBareJid())) {
2424 message.setStatus(Message.STATUS_UNSEND);
2425 } else {
2426 message.setStatus(Message.STATUS_SEND);
2427 }
2428 }
2429 } else {
2430 switch (message.getEncryption()) {
2431 case Message.ENCRYPTION_DECRYPTED:
2432 if (!message.needsUploading()) {
2433 String pgpBody = message.getEncryptedBody();
2434 String decryptedBody = message.getBody();
2435 message.setBody(pgpBody); // TODO might throw NPE
2436 message.setEncryption(Message.ENCRYPTION_PGP);
2437 if (message.edited()) {
2438 message.setBody(decryptedBody);
2439 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
2440 if (!databaseBackend.updateMessage(message, message.getEditedId())) {
2441 Log.e(Config.LOGTAG, "error updated message in DB after edit");
2442 }
2443 updateConversationUi();
2444 if (!waitForPreview && cb != null) cb.run();
2445 return;
2446 } else {
2447 databaseBackend.createMessage(message);
2448 saveInDb = false;
2449 message.setBody(decryptedBody);
2450 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
2451 }
2452 }
2453 break;
2454 case Message.ENCRYPTION_AXOLOTL:
2455 message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
2456 break;
2457 }
2458 }
2459
2460 synchronized (mScheduledMessages) {
2461 if (message.getTimeSent() > System.currentTimeMillis()) {
2462 mScheduledMessages.put(message.getUuid(), message);
2463 scheduleNextIdlePing();
2464 } else {
2465 mScheduledMessages.remove(message.getUuid());
2466 }
2467 }
2468
2469 boolean mucMessage =
2470 conversation.getMode() == Conversation.MODE_MULTI && !message.isPrivateMessage();
2471 if (mucMessage) {
2472 message.setCounterpart(conversation.getMucOptions().getSelf().getFullJid());
2473 }
2474
2475 if (resend) {
2476 if (packet != null && addToConversation) {
2477 if (account.getXmppConnection().getFeatures().sm() || mucMessage) {
2478 markMessage(message, Message.STATUS_UNSEND);
2479 } else {
2480 markMessage(message, Message.STATUS_SEND);
2481 }
2482 }
2483 } else {
2484 if (addToConversation) {
2485 conversation.add(message);
2486 }
2487 if (saveInDb) {
2488 databaseBackend.createMessage(message);
2489 } else if (message.edited()) {
2490 if (!databaseBackend.updateMessage(message, message.getEditedId())) {
2491 Log.e(Config.LOGTAG, "error updated message in DB after edit");
2492 }
2493 }
2494 updateConversationUi();
2495 }
2496 if (packet != null) {
2497 if (delay) {
2498 mMessageGenerator.addDelay(packet, message.getTimeSent());
2499 }
2500 if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
2501 if (this.sendChatStates()) {
2502 packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
2503 }
2504 }
2505 sendMessagePacket(account, packet);
2506 if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.hasCustomEmoji()) {
2507 if (message.getConversation() instanceof Conversation) presenceToMuc((Conversation) message.getConversation());
2508 }
2509 }
2510 if (!waitForPreview && !passedCbOn && cb != null) cb.run();
2511 }
2512
2513 private boolean isJoinInProgress(final Conversation conversation) {
2514 final Account account = conversation.getAccount();
2515 synchronized (account.inProgressConferenceJoins) {
2516 if (conversation.getMode() == Conversational.MODE_MULTI) {
2517 final boolean inProgress = account.inProgressConferenceJoins.contains(conversation);
2518 final boolean pending = account.pendingConferenceJoins.contains(conversation);
2519 final boolean inProgressJoin = inProgress || pending;
2520 if (inProgressJoin) {
2521 Log.d(
2522 Config.LOGTAG,
2523 account.getJid().asBareJid()
2524 + ": holding back message to group. inProgress="
2525 + inProgress
2526 + ", pending="
2527 + pending);
2528 }
2529 return inProgressJoin;
2530 } else {
2531 return false;
2532 }
2533 }
2534 }
2535
2536 private void sendUnsentMessages(final Conversation conversation) {
2537 synchronized (conversation) {
2538 conversation.findWaitingMessages(message -> resendMessage(message, true));
2539 }
2540 }
2541
2542 public void resendMessage(final Message message, final boolean delay) {
2543 sendMessage(message, true, false, delay, false, null);
2544 }
2545
2546 public void resendMessage(final Message message, final boolean delay, final Runnable cb) {
2547 sendMessage(message, true, false, delay, false, cb);
2548 }
2549
2550 public void resendMessage(final Message message, final boolean delay, final boolean previewedLinks) {
2551 sendMessage(message, true, previewedLinks, delay, false, null);
2552 }
2553
2554 public Pair<Account,Account> onboardingIncomplete() {
2555 if (getAccounts().size() != 2) return null;
2556 Account onboarding = null;
2557 Account newAccount = null;
2558 for (final Account account : getAccounts()) {
2559 if (account.getJid().getDomain().equals(Config.ONBOARDING_DOMAIN)) {
2560 onboarding = account;
2561 } else {
2562 newAccount = account;
2563 }
2564 }
2565
2566 if (onboarding != null && newAccount != null) {
2567 return new Pair<>(onboarding, newAccount);
2568 }
2569
2570 return null;
2571 }
2572
2573 public boolean isOnboarding() {
2574 return getAccounts().size() == 1 && getAccounts().get(0).getJid().getDomain().equals(Config.ONBOARDING_DOMAIN);
2575 }
2576
2577 public void requestEasyOnboardingInvite(
2578 final Account account, final EasyOnboardingInvite.OnInviteRequested callback) {
2579 final XmppConnection connection = account.getXmppConnection();
2580 final Jid jid =
2581 connection == null
2582 ? null
2583 : connection.getJidForCommand(Namespace.EASY_ONBOARDING_INVITE);
2584 if (jid == null) {
2585 callback.inviteRequestFailed(
2586 getString(R.string.server_does_not_support_easy_onboarding_invites));
2587 return;
2588 }
2589 final Iq request = new Iq(Iq.Type.SET);
2590 request.setTo(jid);
2591 final Element command = request.addChild("command", Namespace.COMMANDS);
2592 command.setAttribute("node", Namespace.EASY_ONBOARDING_INVITE);
2593 command.setAttribute("action", "execute");
2594 sendIqPacket(
2595 account,
2596 request,
2597 (response) -> {
2598 if (response.getType() == Iq.Type.RESULT) {
2599 final Element resultCommand =
2600 response.findChild("command", Namespace.COMMANDS);
2601 final Element x =
2602 resultCommand == null
2603 ? null
2604 : resultCommand.findChild("x", Namespace.DATA);
2605 if (x != null) {
2606 final Data data = Data.parse(x);
2607 final String uri = data.getValue("uri");
2608 final String landingUrl = data.getValue("landing-url");
2609 if (uri != null) {
2610 final EasyOnboardingInvite invite =
2611 new EasyOnboardingInvite(
2612 jid.getDomain().toString(), uri, landingUrl);
2613 callback.inviteRequested(invite);
2614 return;
2615 }
2616 }
2617 callback.inviteRequestFailed(getString(R.string.unable_to_parse_invite));
2618 Log.d(Config.LOGTAG, response.toString());
2619 } else if (response.getType() == Iq.Type.ERROR) {
2620 callback.inviteRequestFailed(IqParser.errorMessage(response));
2621 } else {
2622 callback.inviteRequestFailed(getString(R.string.remote_server_timeout));
2623 }
2624 });
2625 }
2626
2627 public void fetchBookmarks(final Account account) {
2628 final Iq iqPacket = new Iq(Iq.Type.GET);
2629 iqPacket.addExtension(new PrivateStorage()).addExtension(new Storage());
2630 final Consumer<Iq> callback =
2631 (response) -> {
2632 if (response.getType() == Iq.Type.RESULT) {
2633 final var privateStorage = response.getExtension(PrivateStorage.class);
2634 if (privateStorage == null) {
2635 return;
2636 }
2637 final var bookmarkStorage = privateStorage.getExtension(Storage.class);
2638 Map<Jid, Bookmark> bookmarks =
2639 Bookmark.parseFromStorage(bookmarkStorage, account);
2640 processBookmarksInitial(account, bookmarks, false);
2641 } else {
2642 Log.d(
2643 Config.LOGTAG,
2644 account.getJid().asBareJid() + ": could not fetch bookmarks");
2645 }
2646 };
2647 sendIqPacket(account, iqPacket, callback);
2648 }
2649
2650 public void fetchBookmarks2(final Account account) {
2651 final Iq retrieve = mIqGenerator.retrieveBookmarks();
2652 sendIqPacket(
2653 account,
2654 retrieve,
2655 (response) -> {
2656 if (response.getType() == Iq.Type.RESULT) {
2657 final var pubsub = response.getExtension(PubSub.class);
2658 final Map<Jid, Bookmark> bookmarks =
2659 Bookmark.parseFromPubSub(pubsub, account);
2660 processBookmarksInitial(account, bookmarks, true);
2661 }
2662 });
2663 }
2664
2665 public void fetchMessageDisplayedSynchronization(final Account account) {
2666 Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds");
2667 final var retrieve = mIqGenerator.retrieveMds();
2668 sendIqPacket(
2669 account,
2670 retrieve,
2671 (response) -> {
2672 if (response.getType() != Iq.Type.RESULT) {
2673 return;
2674 }
2675 final var pubsub = response.getExtension(PubSub.class);
2676 if (pubsub == null) {
2677 return;
2678 }
2679 final var items = pubsub.getItems();
2680 if (items == null) {
2681 return;
2682 }
2683 if (Namespace.MDS_DISPLAYED.equals(items.getNode())) {
2684 for (final var item :
2685 items.getItemMap(
2686 im.conversations.android.xmpp.model.mds.Displayed
2687 .class)
2688 .entrySet()) {
2689 processMdsItem(account, item);
2690 }
2691 }
2692 });
2693 }
2694
2695 public void processMdsItem(final Account account, final Map.Entry<String, Displayed> item) {
2696 final Jid jid = Jid.Invalid.getNullForInvalid(Jid.ofOrInvalid(item.getKey()));
2697 if (jid == null) {
2698 return;
2699 }
2700 final var displayed = item.getValue();
2701 final var stanzaId = displayed.getStanzaId();
2702 final String id = stanzaId == null ? null : stanzaId.getId();
2703 final Conversation conversation = find(account, jid);
2704 if (id != null && conversation != null) {
2705 conversation.setDisplayState(id);
2706 markReadUpToStanzaId(conversation, id);
2707 }
2708 }
2709
2710 public void markReadUpToStanzaId(final Conversation conversation, final String stanzaId) {
2711 final Message message = conversation.findMessageWithServerMsgId(stanzaId);
2712 if (message == null) { // do we want to check if isRead?
2713 return;
2714 }
2715 markReadUpTo(conversation, message);
2716 }
2717
2718 public void markReadUpTo(final Conversation conversation, final Message message) {
2719 final boolean isDismissNotification = isDismissNotification(message);
2720 final var uuid = message.getUuid();
2721 Log.d(
2722 Config.LOGTAG,
2723 conversation.getAccount().getJid().asBareJid()
2724 + ": mark "
2725 + conversation.getJid().asBareJid()
2726 + " as read up to "
2727 + uuid);
2728 markRead(conversation, uuid, isDismissNotification);
2729 }
2730
2731 private static boolean isDismissNotification(final Message message) {
2732 Message next = message.next();
2733 while (next != null) {
2734 if (message.getStatus() == Message.STATUS_RECEIVED) {
2735 return false;
2736 }
2737 next = next.next();
2738 }
2739 return true;
2740 }
2741
2742 public void processBookmarksInitial(
2743 final Account account, final Map<Jid, Bookmark> bookmarks, final boolean pep) {
2744 final Set<Jid> previousBookmarks = account.getBookmarkedJids();
2745 for (final Bookmark bookmark : bookmarks.values()) {
2746 previousBookmarks.remove(bookmark.getJid().asBareJid());
2747 processModifiedBookmark(bookmark, pep);
2748 }
2749 if (pep) {
2750 processDeletedBookmarks(account, previousBookmarks);
2751 }
2752 account.setBookmarks(bookmarks);
2753 }
2754
2755 public void processDeletedBookmarks(final Account account, final Collection<Jid> bookmarks) {
2756 Log.d(
2757 Config.LOGTAG,
2758 account.getJid().asBareJid()
2759 + ": "
2760 + bookmarks.size()
2761 + " bookmarks have been removed");
2762 for (final Jid bookmark : bookmarks) {
2763 processDeletedBookmark(account, bookmark);
2764 }
2765 }
2766
2767 public void processDeletedBookmark(final Account account, final Jid jid) {
2768 final Conversation conversation = find(account, jid);
2769 if (conversation == null) {
2770 return;
2771 }
2772 Log.d(
2773 Config.LOGTAG,
2774 account.getJid().asBareJid() + ": archiving MUC " + jid + " after PEP update");
2775 archiveConversation(conversation, false);
2776 }
2777
2778 private void processModifiedBookmark(final Bookmark bookmark, final boolean pep) {
2779 final Account account = bookmark.getAccount();
2780 Conversation conversation = find(bookmark);
2781 if (conversation != null) {
2782 if (conversation.getMode() != Conversation.MODE_MULTI) {
2783 return;
2784 }
2785 bookmark.setConversation(conversation);
2786 if (pep && !bookmark.autojoin()) {
2787 Log.d(
2788 Config.LOGTAG,
2789 account.getJid().asBareJid()
2790 + ": archiving conference ("
2791 + conversation.getJid()
2792 + ") after receiving pep");
2793 archiveConversation(conversation, false);
2794 } else {
2795 final MucOptions mucOptions = conversation.getMucOptions();
2796 if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) {
2797 final String current = mucOptions.getActualNick();
2798 final String proposed = mucOptions.getProposedNickPure();
2799 if (current != null && !current.equals(proposed)) {
2800 Log.d(
2801 Config.LOGTAG,
2802 account.getJid().asBareJid()
2803 + ": proposed nick changed after bookmark push "
2804 + current
2805 + "->"
2806 + proposed);
2807 joinMuc(conversation);
2808 }
2809 } else {
2810 checkMucRequiresRename(conversation);
2811 }
2812 }
2813 } else if (bookmark.autojoin()) {
2814 conversation =
2815 findOrCreateConversation(account, bookmark.getFullJid(), true, true, false);
2816 bookmark.setConversation(conversation);
2817 }
2818 }
2819
2820 public void processModifiedBookmark(final Bookmark bookmark) {
2821 processModifiedBookmark(bookmark, true);
2822 }
2823
2824 public void ensureBookmarkIsAutoJoin(final Conversation conversation) {
2825 final var account = conversation.getAccount();
2826 final var existingBookmark = conversation.getBookmark();
2827 if (existingBookmark == null) {
2828 final var bookmark = new Bookmark(account, conversation.getJid().asBareJid());
2829 bookmark.setAutojoin(true);
2830 createBookmark(account, bookmark);
2831 } else {
2832 if (existingBookmark.autojoin()) {
2833 return;
2834 }
2835 existingBookmark.setAutojoin(true);
2836 createBookmark(account, existingBookmark);
2837 }
2838 }
2839
2840 public void createBookmark(final Account account, final Bookmark bookmark) {
2841 account.putBookmark(bookmark);
2842 final XmppConnection connection = account.getXmppConnection();
2843 if (connection == null) {
2844 Log.d(
2845 Config.LOGTAG,
2846 account.getJid().asBareJid() + ": no connection. ignoring bookmark creation");
2847 } else if (connection.getFeatures().bookmarks2()) {
2848 Log.d(
2849 Config.LOGTAG,
2850 account.getJid().asBareJid() + ": pushing bookmark via Bookmarks 2");
2851 final Element item = mIqGenerator.publishBookmarkItem(bookmark);
2852 pushNodeAndEnforcePublishOptions(
2853 account,
2854 Namespace.BOOKMARKS2,
2855 item,
2856 bookmark.getJid().asBareJid().toString(),
2857 PublishOptions.persistentWhitelistAccessMaxItems());
2858 } else if (connection.getFeatures().bookmarksConversion()) {
2859 pushBookmarksPep(account);
2860 } else {
2861 pushBookmarksPrivateXml(account);
2862 }
2863 }
2864
2865 public void deleteBookmark(final Account account, final Bookmark bookmark) {
2866 if (bookmark.getJid().toString().equals("discuss@conference.soprani.ca")) {
2867 getPreferences().edit().putBoolean("cheogram_sopranica_bookmark_deleted", true).apply();
2868 }
2869 account.removeBookmark(bookmark);
2870 final XmppConnection connection = account.getXmppConnection();
2871 if (connection == null) return;
2872
2873 if (connection.getFeatures().bookmarks2()) {
2874 final Iq request =
2875 mIqGenerator.deleteItem(
2876 Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toString());
2877 Log.d(
2878 Config.LOGTAG,
2879 account.getJid().asBareJid() + ": removing bookmark via Bookmarks 2");
2880 sendIqPacket(
2881 account,
2882 request,
2883 (response) -> {
2884 if (response.getType() == Iq.Type.ERROR) {
2885 Log.d(
2886 Config.LOGTAG,
2887 account.getJid().asBareJid()
2888 + ": unable to delete bookmark "
2889 + response.getErrorCondition());
2890 }
2891 });
2892 } else if (connection.getFeatures().bookmarksConversion()) {
2893 pushBookmarksPep(account);
2894 } else {
2895 pushBookmarksPrivateXml(account);
2896 }
2897 }
2898
2899 private void pushBookmarksPrivateXml(Account account) {
2900 if (!account.areBookmarksLoaded()) return;
2901
2902 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml");
2903 final Iq iqPacket = new Iq(Iq.Type.SET);
2904 Element query = iqPacket.query("jabber:iq:private");
2905 Element storage = query.addChild("storage", "storage:bookmarks");
2906 for (final Bookmark bookmark : account.getBookmarks()) {
2907 storage.addChild(bookmark);
2908 }
2909 sendIqPacket(account, iqPacket, mDefaultIqHandler);
2910 }
2911
2912 private void pushBookmarksPep(Account account) {
2913 if (!account.areBookmarksLoaded()) return;
2914
2915 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via pep");
2916 final Element storage = new Element("storage", "storage:bookmarks");
2917 for (final Bookmark bookmark : account.getBookmarks()) {
2918 storage.addChild(bookmark);
2919 }
2920 pushNodeAndEnforcePublishOptions(
2921 account,
2922 Namespace.BOOKMARKS,
2923 storage,
2924 "current",
2925 PublishOptions.persistentWhitelistAccess());
2926 }
2927
2928 private void pushNodeAndEnforcePublishOptions(
2929 final Account account,
2930 final String node,
2931 final Element element,
2932 final String id,
2933 final Bundle options) {
2934 pushNodeAndEnforcePublishOptions(account, node, element, id, options, true);
2935 }
2936
2937 private void pushNodeAndEnforcePublishOptions(
2938 final Account account,
2939 final String node,
2940 final Element element,
2941 final String id,
2942 final Bundle options,
2943 final boolean retry) {
2944 final Iq packet = mIqGenerator.publishElement(node, element, id, options);
2945 sendIqPacket(
2946 account,
2947 packet,
2948 (response) -> {
2949 if (response.getType() == Iq.Type.RESULT) {
2950 return;
2951 }
2952 if (retry && PublishOptions.preconditionNotMet(response)) {
2953 pushNodeConfiguration(
2954 account,
2955 node,
2956 options,
2957 new OnConfigurationPushed() {
2958 @Override
2959 public void onPushSucceeded() {
2960 pushNodeAndEnforcePublishOptions(
2961 account, node, element, id, options, false);
2962 }
2963
2964 @Override
2965 public void onPushFailed() {
2966 Log.d(
2967 Config.LOGTAG,
2968 account.getJid().asBareJid()
2969 + ": unable to push node configuration ("
2970 + node
2971 + ")");
2972 }
2973 });
2974 } else {
2975 Log.d(
2976 Config.LOGTAG,
2977 account.getJid().asBareJid()
2978 + ": error publishing "
2979 + node
2980 + " (retry="
2981 + retry
2982 + ") "
2983 + response);
2984 }
2985 });
2986 }
2987
2988 private void restoreFromDatabase() {
2989 synchronized (this.conversations) {
2990 final Map<String, Account> accountLookupTable =
2991 ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
2992 Log.d(Config.LOGTAG, "restoring conversations...");
2993 final long startTimeConversationsRestore = SystemClock.elapsedRealtime();
2994 this.conversations.addAll(
2995 databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
2996 for (Iterator<Conversation> iterator = conversations.listIterator();
2997 iterator.hasNext(); ) {
2998 Conversation conversation = iterator.next();
2999 Account account = accountLookupTable.get(conversation.getAccountUuid());
3000 if (account != null) {
3001 conversation.setAccount(account);
3002 } else {
3003 Log.e(Config.LOGTAG, "unable to restore Conversations with " + conversation.getJid());
3004 conversations.remove(conversation);
3005 }
3006 }
3007 long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore;
3008 Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms");
3009 Runnable runnable = () -> {
3010 if (DatabaseBackend.requiresMessageIndexRebuild()) {
3011 DatabaseBackend.getInstance(this).rebuildMessagesIndex();
3012 }
3013 mutedMucUsers = databaseBackend.loadMutedMucUsers();
3014 final long deletionDate = getAutomaticMessageDeletionDate();
3015 mLastExpiryRun.set(SystemClock.elapsedRealtime());
3016 if (deletionDate > 0) {
3017 Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate));
3018 databaseBackend.expireOldMessages(deletionDate);
3019 }
3020 Log.d(Config.LOGTAG, "restoring roster...");
3021 for (final Account account : accounts) {
3022 databaseBackend.readRoster(account.getRoster());
3023 account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage
3024 }
3025 getDrawableCache().evictAll();
3026 loadPhoneContacts();
3027 Log.d(Config.LOGTAG, "restoring messages...");
3028 final long startMessageRestore = SystemClock.elapsedRealtime();
3029 final Conversation quickLoad = QuickLoader.get(this.conversations);
3030 if (quickLoad != null) {
3031 restoreMessages(quickLoad);
3032 updateConversationUi();
3033 final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
3034 Log.d(Config.LOGTAG, "quickly restored " + quickLoad.getName() + " after " + diffMessageRestore + "ms");
3035 }
3036 for (Conversation conversation : this.conversations) {
3037 if (quickLoad != conversation) {
3038 restoreMessages(conversation);
3039 }
3040 }
3041 mNotificationService.finishBacklog();
3042 restoredFromDatabaseLatch.countDown();
3043 final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
3044 Log.d(Config.LOGTAG, "finished restoring messages in " + diffMessageRestore + "ms");
3045 updateConversationUi();
3046 };
3047 mDatabaseReaderExecutor.execute(runnable); //will contain one write command (expiry) but that's fine
3048 }
3049 }
3050
3051 private void restoreMessages(Conversation conversation) {
3052 conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
3053 conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
3054 conversation.findMessagesAndCallsToNotify(mNotificationService::pushFromBacklog);
3055 }
3056
3057 public void loadPhoneContacts() {
3058 mContactMergerExecutor.execute(
3059 () -> {
3060 final Map<Jid, JabberIdContact> contacts = JabberIdContact.load(this);
3061 Log.d(Config.LOGTAG, "start merging phone contacts with roster");
3062 for (final Account account : accounts) {
3063 final List<Contact> withSystemAccounts =
3064 account.getRoster().getWithSystemAccounts(JabberIdContact.class);
3065 for (final JabberIdContact jidContact : contacts.values()) {
3066 final Contact contact =
3067 account.getRoster().getContact(jidContact.getJid());
3068 boolean needsCacheClean = contact.setPhoneContact(jidContact);
3069 if (needsCacheClean) {
3070 getAvatarService().clear(contact);
3071 }
3072 withSystemAccounts.remove(contact);
3073 }
3074 for (final Contact contact : withSystemAccounts) {
3075 boolean needsCacheClean =
3076 contact.unsetPhoneContact(JabberIdContact.class);
3077 if (needsCacheClean) {
3078 getAvatarService().clear(contact);
3079 }
3080 }
3081 }
3082 Log.d(Config.LOGTAG, "finished merging phone contacts");
3083 mShortcutService.refresh(
3084 mInitialAddressbookSyncCompleted.compareAndSet(false, true));
3085 updateRosterUi(UpdateRosterReason.PUSH);
3086 mQuickConversationsService.considerSync();
3087 });
3088 }
3089
3090 public void syncRoster(final Account account) {
3091 mRosterSyncTaskManager.execute(account, () -> {
3092 unregisterPhoneAccounts(account);
3093 databaseBackend.writeRoster(account.getRoster());
3094 try { Thread.sleep(500); } catch (InterruptedException e) { }
3095 });
3096 }
3097
3098 public List<Conversation> getConversations() {
3099 return this.conversations;
3100 }
3101
3102 private void markFileDeleted(final File file) {
3103 synchronized (FILENAMES_TO_IGNORE_DELETION) {
3104 if (FILENAMES_TO_IGNORE_DELETION.remove(file.getAbsolutePath())) {
3105 Log.d(Config.LOGTAG, "ignored deletion of " + file.getAbsolutePath());
3106 return;
3107 }
3108 }
3109 final boolean isInternalFile = fileBackend.isInternalFile(file);
3110 final List<String> uuids = databaseBackend.markFileAsDeleted(file, isInternalFile);
3111 Log.d(
3112 Config.LOGTAG,
3113 "deleted file "
3114 + file.getAbsolutePath()
3115 + " internal="
3116 + isInternalFile
3117 + ", database hits="
3118 + uuids.size());
3119 markUuidsAsDeletedFiles(uuids);
3120 }
3121
3122 private void markUuidsAsDeletedFiles(List<String> uuids) {
3123 boolean deleted = false;
3124 for (Conversation conversation : getConversations()) {
3125 deleted |= conversation.markAsDeleted(uuids);
3126 }
3127 for (final String uuid : uuids) {
3128 evictPreview(uuid);
3129 }
3130 if (deleted) {
3131 updateConversationUi();
3132 }
3133 }
3134
3135 private void markChangedFiles(List<DatabaseBackend.FilePathInfo> infos) {
3136 boolean changed = false;
3137 for (Conversation conversation : getConversations()) {
3138 changed |= conversation.markAsChanged(infos);
3139 }
3140 if (changed) {
3141 updateConversationUi();
3142 }
3143 }
3144
3145 public void populateWithOrderedConversations(final List<Conversation> list) {
3146 populateWithOrderedConversations(list, true, true);
3147 }
3148
3149 public void populateWithOrderedConversations(
3150 final List<Conversation> list, final boolean includeNoFileUpload) {
3151 populateWithOrderedConversations(list, includeNoFileUpload, true);
3152 }
3153
3154 public void populateWithOrderedConversations(
3155 final List<Conversation> list, final boolean includeNoFileUpload, final boolean sort) {
3156 final List<String> orderedUuids;
3157 if (sort) {
3158 orderedUuids = null;
3159 } else {
3160 orderedUuids = new ArrayList<>();
3161 for (Conversation conversation : list) {
3162 orderedUuids.add(conversation.getUuid());
3163 }
3164 }
3165 list.clear();
3166 if (includeNoFileUpload) {
3167 list.addAll(getConversations());
3168 } else {
3169 for (Conversation conversation : getConversations()) {
3170 if (conversation.getMode() == Conversation.MODE_SINGLE
3171 || (conversation.getAccount().httpUploadAvailable()
3172 && conversation.getMucOptions().participating())) {
3173 list.add(conversation);
3174 }
3175 }
3176 }
3177 try {
3178 if (orderedUuids != null) {
3179 Collections.sort(
3180 list,
3181 (a, b) -> {
3182 final int indexA = orderedUuids.indexOf(a.getUuid());
3183 final int indexB = orderedUuids.indexOf(b.getUuid());
3184 if (indexA == -1 || indexB == -1 || indexA == indexB) {
3185 return a.compareTo(b);
3186 }
3187 return indexA - indexB;
3188 });
3189 } else {
3190 Collections.sort(list);
3191 }
3192 } catch (IllegalArgumentException e) {
3193 // ignore
3194 }
3195 }
3196
3197 public void loadMoreMessages(
3198 final Conversation conversation,
3199 final long timestamp,
3200 final OnMoreMessagesLoaded callback) {
3201 if (XmppConnectionService.this
3202 .getMessageArchiveService()
3203 .queryInProgress(conversation, callback)) {
3204 return;
3205 } else if (timestamp == 0) {
3206 return;
3207 }
3208 Log.d(
3209 Config.LOGTAG,
3210 "load more messages for "
3211 + conversation.getName()
3212 + " prior to "
3213 + MessageGenerator.getTimestamp(timestamp));
3214 final Runnable runnable =
3215 () -> {
3216 final Account account = conversation.getAccount();
3217 List<Message> messages =
3218 databaseBackend.getMessages(conversation, 50, timestamp);
3219 if (messages.size() > 0) {
3220 conversation.addAll(0, messages);
3221 callback.onMoreMessagesLoaded(messages.size(), conversation);
3222 } else if (conversation.hasMessagesLeftOnServer()
3223 && account.isOnlineAndConnected()
3224 && conversation.getLastClearHistory().getTimestamp() == 0) {
3225 final boolean mamAvailable;
3226 if (conversation.getMode() == Conversation.MODE_SINGLE) {
3227 mamAvailable =
3228 account.getXmppConnection().getFeatures().mam()
3229 && !conversation.getContact().isBlocked();
3230 } else {
3231 mamAvailable = conversation.getMucOptions().mamSupport();
3232 }
3233 if (mamAvailable) {
3234 MessageArchiveService.Query query =
3235 getMessageArchiveService()
3236 .query(
3237 conversation,
3238 new MamReference(0),
3239 timestamp,
3240 false);
3241 if (query != null) {
3242 query.setCallback(callback);
3243 callback.informUser(R.string.fetching_history_from_server);
3244 } else {
3245 callback.informUser(R.string.not_fetching_history_retention_period);
3246 }
3247 }
3248 }
3249 };
3250 mDatabaseReaderExecutor.execute(runnable);
3251 }
3252
3253 public List<Account> getAccounts() {
3254 return this.accounts;
3255 }
3256
3257 /**
3258 * This will find all conferences with the contact as member and also the conference that is the
3259 * contact (that 'fake' contact is used to store the avatar)
3260 */
3261 public List<Conversation> findAllConferencesWith(Contact contact) {
3262 final ArrayList<Conversation> results = new ArrayList<>();
3263 for (final Conversation c : conversations) {
3264 if (c.getMode() != Conversation.MODE_MULTI) {
3265 continue;
3266 }
3267 final MucOptions mucOptions = c.getMucOptions();
3268 if (c.getJid().asBareJid().equals(contact.getJid().asBareJid())
3269 || (mucOptions != null && mucOptions.isContactInRoom(contact))) {
3270 results.add(c);
3271 }
3272 }
3273 return results;
3274 }
3275
3276 public Conversation find(final Contact contact) {
3277 for (final Conversation conversation : this.conversations) {
3278 if (conversation.getContact() == contact) {
3279 return conversation;
3280 }
3281 }
3282 return null;
3283 }
3284
3285 public Conversation find(
3286 final Iterable<Conversation> haystack, final Account account, final Jid jid) {
3287 if (jid == null) {
3288 return null;
3289 }
3290 for (final Conversation conversation : haystack) {
3291 if ((account == null || conversation.getAccount() == account)
3292 && (conversation.getJid().asBareJid().equals(jid.asBareJid()))) {
3293 return conversation;
3294 }
3295 }
3296 return null;
3297 }
3298
3299 public boolean isConversationsListEmpty(final Conversation ignore) {
3300 synchronized (this.conversations) {
3301 final int size = this.conversations.size();
3302 return size == 0 || size == 1 && this.conversations.get(0) == ignore;
3303 }
3304 }
3305
3306 public boolean isConversationStillOpen(final Conversation conversation) {
3307 synchronized (this.conversations) {
3308 for (Conversation current : this.conversations) {
3309 if (current == conversation) {
3310 return true;
3311 }
3312 }
3313 }
3314 return false;
3315 }
3316
3317 public void maybeRegisterWithMuc(Conversation c, String nickArg) {
3318 final var nick = nickArg == null ? c.getMucOptions().getSelf().getFullJid().getResource() : nickArg;
3319 final var register = new Iq(Iq.Type.GET);
3320 register.query(Namespace.REGISTER);
3321 register.setTo(c.getJid().asBareJid());
3322 sendIqPacket(c.getAccount(), register, (response) -> {
3323 if (response.getType() == Iq.Type.RESULT) {
3324 final Element query = response.query(Namespace.REGISTER);
3325 String username = query.findChildContent("username", Namespace.REGISTER);
3326 if (username == null) username = query.findChildContent("nick", Namespace.REGISTER);
3327 if (username != null && username.equals(nick)) {
3328 // Already registered with this nick, done
3329 Log.d(Config.LOGTAG, "Already registered with " + c.getJid().asBareJid() + " as " + username);
3330 return;
3331 }
3332 Data form = Data.parse(query.findChild("x", Namespace.DATA));
3333 if (form != null) {
3334 final var field = form.getFieldByName("muc#register_roomnick");
3335 if (field != null && nick.equals(field.getValue())) {
3336 Log.d(Config.LOGTAG, "Already registered with " + c.getJid().asBareJid() + " as " + field.getValue());
3337 return;
3338 }
3339 }
3340 if (form == null || !"form".equals(form.getFormType()) || !form.getFields().stream().anyMatch(f -> f.isRequired() && !"muc#register_roomnick".equals(f.getFieldName()))) {
3341 // No form, result form, or no required fields other than nickname, let's just send nickname
3342 if (form == null || !"form".equals(form.getFormType())) {
3343 form = new Data();
3344 form.put("FORM_TYPE", "http://jabber.org/protocol/muc#register");
3345 }
3346 form.put("muc#register_roomnick", nick);
3347 form.submit();
3348 final var finish = new Iq(Iq.Type.SET);
3349 finish.query(Namespace.REGISTER).addChild(form);
3350 finish.setTo(c.getJid().asBareJid());
3351 sendIqPacket(c.getAccount(), finish, (response2) -> {
3352 if (response.getType() == Iq.Type.RESULT) {
3353 Log.w(Config.LOGTAG, "Success registering with channel " + c.getJid().asBareJid() + "/" + nick);
3354 } else {
3355 Log.w(Config.LOGTAG, "Error registering with channel: " + response2);
3356 }
3357 });
3358 } else {
3359 // TODO: offer registration form to user
3360 Log.d(Config.LOGTAG, "Complex registration form for " + c.getJid().asBareJid() + ": " + response);
3361 }
3362 } else {
3363 // We said maybe. Guess not
3364 Log.d(Config.LOGTAG, "Could not register with " + c.getJid().asBareJid() + ": " + response);
3365 }
3366 });
3367 }
3368
3369 public void deregisterWithMuc(Conversation c) {
3370 final Iq register = new Iq(Iq.Type.GET);
3371 register.query(Namespace.REGISTER).addChild("remove");
3372 register.setTo(c.getJid().asBareJid());
3373 sendIqPacket(c.getAccount(), register, (response) -> {
3374 if (response.getType() == Iq.Type.RESULT) {
3375 Log.d(Config.LOGTAG, "deregistered with " + c.getJid().asBareJid());
3376 } else {
3377 Log.w(Config.LOGTAG, "Could not deregister with " + c.getJid().asBareJid() + ": " + response);
3378 }
3379 });
3380 }
3381
3382 public Conversation findOrCreateConversation(
3383 Account account, Jid jid, boolean muc, final boolean async) {
3384 return this.findOrCreateConversation(account, jid, muc, false, async);
3385 }
3386
3387 public Conversation findOrCreateConversation(
3388 final Account account,
3389 final Jid jid,
3390 final boolean muc,
3391 final boolean joinAfterCreate,
3392 final boolean async) {
3393 return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, null, async, null);
3394 }
3395
3396 public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final MessageArchiveService.Query query, final boolean async) {
3397 return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, query, async, null);
3398 }
3399
3400 public Conversation findOrCreateConversation(
3401 final Account account,
3402 final Jid jid,
3403 final boolean muc,
3404 final boolean joinAfterCreate,
3405 final MessageArchiveService.Query query,
3406 final boolean async,
3407 final String password) {
3408 synchronized (this.conversations) {
3409 final var cached = find(account, jid);
3410 if (cached != null) {
3411 return cached;
3412 }
3413 final var existing = databaseBackend.findConversation(account, jid);
3414 final Conversation conversation;
3415 final boolean loadMessagesFromDb;
3416 if (existing != null) {
3417 conversation = existing;
3418 if (password != null) conversation.getMucOptions().setPassword(password);
3419 loadMessagesFromDb = restoreFromArchive(conversation, jid, muc);
3420 } else {
3421 String conversationName;
3422 final Contact contact = account.getRoster().getContact(jid);
3423 if (contact != null) {
3424 conversationName = contact.getDisplayName();
3425 } else {
3426 conversationName = jid.getLocal();
3427 }
3428 if (muc) {
3429 conversation =
3430 new Conversation(
3431 conversationName, account, jid, Conversation.MODE_MULTI);
3432 } else {
3433 conversation =
3434 new Conversation(
3435 conversationName,
3436 account,
3437 jid.asBareJid(),
3438 Conversation.MODE_SINGLE);
3439 }
3440 if (password != null) conversation.getMucOptions().setPassword(password);
3441 this.databaseBackend.createConversation(conversation);
3442 loadMessagesFromDb = false;
3443 }
3444 if (async) {
3445 mDatabaseReaderExecutor.execute(
3446 () ->
3447 postProcessConversation(
3448 conversation, loadMessagesFromDb, joinAfterCreate, query));
3449 } else {
3450 postProcessConversation(conversation, loadMessagesFromDb, joinAfterCreate, query);
3451 }
3452 this.conversations.add(conversation);
3453 updateConversationUi();
3454 return conversation;
3455 }
3456 }
3457
3458 public Conversation findConversationByUuidReliable(final String uuid) {
3459 final var cached = findConversationByUuid(uuid);
3460 if (cached != null) {
3461 return cached;
3462 }
3463 final var existing = databaseBackend.findConversation(uuid);
3464 if (existing == null) {
3465 return null;
3466 }
3467 Log.d(Config.LOGTAG, "restoring conversation with " + existing.getJid() + " from DB");
3468 final Map<String, Account> accounts =
3469 ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
3470 final var account = accounts.get(existing.getAccountUuid());
3471 if (account == null) {
3472 Log.d(Config.LOGTAG, "could not find account " + existing.getAccountUuid());
3473 return null;
3474 }
3475 existing.setAccount(account);
3476 final var loadMessagesFromDb = restoreFromArchive(existing);
3477 mDatabaseReaderExecutor.execute(
3478 () ->
3479 postProcessConversation(
3480 existing,
3481 loadMessagesFromDb,
3482 existing.getMode() == Conversational.MODE_MULTI,
3483 null));
3484 this.conversations.add(existing);
3485 if (existing.getMode() == Conversational.MODE_MULTI) {
3486 ensureBookmarkIsAutoJoin(existing);
3487 }
3488 updateConversationUi();
3489 return existing;
3490 }
3491
3492 private boolean restoreFromArchive(
3493 final Conversation conversation, final Jid jid, final boolean muc) {
3494 if (muc) {
3495 conversation.setMode(Conversation.MODE_MULTI);
3496 conversation.setContactJid(jid);
3497 } else {
3498 conversation.setMode(Conversation.MODE_SINGLE);
3499 conversation.setContactJid(jid.asBareJid());
3500 }
3501 return restoreFromArchive(conversation);
3502 }
3503
3504 private boolean restoreFromArchive(final Conversation conversation) {
3505 conversation.setStatus(Conversation.STATUS_AVAILABLE);
3506 databaseBackend.updateConversation(conversation);
3507 return conversation.messagesLoaded.compareAndSet(true, false);
3508 }
3509
3510 private void postProcessConversation(
3511 final Conversation c,
3512 final boolean loadMessagesFromDb,
3513 final boolean joinAfterCreate,
3514 final MessageArchiveService.Query query) {
3515 final var singleMode = c.getMode() == Conversational.MODE_SINGLE;
3516 final var account = c.getAccount();
3517 if (loadMessagesFromDb) {
3518 c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE));
3519 updateConversationUi();
3520 c.messagesLoaded.set(true);
3521 }
3522 if (account.getXmppConnection() != null
3523 && !c.getContact().isBlocked()
3524 && account.getXmppConnection().getFeatures().mam()
3525 && singleMode) {
3526 if (query == null) {
3527 mMessageArchiveService.query(c);
3528 } else {
3529 if (query.getConversation() == null) {
3530 mMessageArchiveService.query(c, query.getStart(), query.isCatchup());
3531 }
3532 }
3533 }
3534 if (joinAfterCreate) {
3535 joinMuc(c);
3536 }
3537 }
3538
3539 public void archiveConversation(Conversation conversation) {
3540 archiveConversation(conversation, true);
3541 }
3542
3543 private void archiveConversation(
3544 Conversation conversation, final boolean maySynchronizeWithBookmarks) {
3545 if (isOnboarding()) return;
3546
3547 getNotificationService().clear(conversation);
3548 conversation.setStatus(Conversation.STATUS_ARCHIVED);
3549 conversation.setNextMessage(null);
3550 synchronized (this.conversations) {
3551 getMessageArchiveService().kill(conversation);
3552 if (conversation.getMode() == Conversation.MODE_MULTI) {
3553 if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
3554 final Bookmark bookmark = conversation.getBookmark();
3555 if (maySynchronizeWithBookmarks && bookmark != null) {
3556 if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) {
3557 Account account = bookmark.getAccount();
3558 bookmark.setConversation(null);
3559 deleteBookmark(account, bookmark);
3560 } else if (bookmark.autojoin()) {
3561 bookmark.setAutojoin(false);
3562 createBookmark(bookmark.getAccount(), bookmark);
3563 }
3564 }
3565 }
3566 deregisterWithMuc(conversation);
3567 leaveMuc(conversation);
3568 } else {
3569 if (conversation
3570 .getContact()
3571 .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
3572 stopPresenceUpdatesTo(conversation.getContact());
3573 }
3574 }
3575 updateConversation(conversation);
3576 this.conversations.remove(conversation);
3577 updateConversationUi();
3578 }
3579 }
3580
3581 public void stopPresenceUpdatesTo(Contact contact) {
3582 Log.d(Config.LOGTAG, "Canceling presence request from " + contact.getJid().toString());
3583 sendPresencePacket(contact.getAccount(), mPresenceGenerator.stopPresenceUpdatesTo(contact));
3584 contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
3585 }
3586
3587 public void createAccount(final Account account) {
3588 account.initAccountServices(this);
3589 databaseBackend.createAccount(account);
3590 if (CallIntegration.hasSystemFeature(this)) {
3591 CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
3592 }
3593 this.accounts.add(account);
3594 this.reconnectAccountInBackground(account);
3595 updateAccountUi();
3596 syncEnabledAccountSetting();
3597 toggleForegroundService();
3598 }
3599
3600 private void syncEnabledAccountSetting() {
3601 final boolean hasEnabledAccounts = hasEnabledAccounts();
3602 getPreferences()
3603 .edit()
3604 .putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts)
3605 .apply();
3606 toggleSetProfilePictureActivity(hasEnabledAccounts);
3607 }
3608
3609 private void toggleSetProfilePictureActivity(final boolean enabled) {
3610 try {
3611 final ComponentName name =
3612 new ComponentName(this, ChooseAccountForProfilePictureActivity.class);
3613 final int targetState =
3614 enabled
3615 ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
3616 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
3617 getPackageManager()
3618 .setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP);
3619 } catch (IllegalStateException e) {
3620 Log.d(Config.LOGTAG, "unable to toggle profile picture activity");
3621 }
3622 }
3623
3624 public boolean reconfigurePushDistributor() {
3625 return this.unifiedPushBroker.reconfigurePushDistributor();
3626 }
3627
3628 private Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints(
3629 final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger) {
3630 return this.unifiedPushBroker.renewUnifiedPushEndpoints(pushTargetMessenger);
3631 }
3632
3633 public Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints() {
3634 return this.unifiedPushBroker.renewUnifiedPushEndpoints(null);
3635 }
3636
3637 public UnifiedPushBroker getUnifiedPushBroker() {
3638 return this.unifiedPushBroker;
3639 }
3640
3641 private void provisionAccount(final String address, final String password) {
3642 final Jid jid = Jid.of(address);
3643 final Account account = new Account(jid, password);
3644 account.setOption(Account.OPTION_DISABLED, true);
3645 Log.d(Config.LOGTAG, jid.asBareJid().toString() + ": provisioning account");
3646 createAccount(account);
3647 }
3648
3649 public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
3650 new Thread(
3651 () -> {
3652 try {
3653 final X509Certificate[] chain =
3654 KeyChain.getCertificateChain(this, alias);
3655 final X509Certificate cert =
3656 chain != null && chain.length > 0 ? chain[0] : null;
3657 if (cert == null) {
3658 callback.informUser(R.string.unable_to_parse_certificate);
3659 return;
3660 }
3661 Pair<Jid, String> info = CryptoHelper.extractJidAndName(cert);
3662 if (info == null) {
3663 callback.informUser(R.string.certificate_does_not_contain_jid);
3664 return;
3665 }
3666 if (findAccountByJid(info.first) == null) {
3667 final Account account = new Account(info.first, "");
3668 account.setPrivateKeyAlias(alias);
3669 account.setOption(Account.OPTION_DISABLED, true);
3670 account.setOption(Account.OPTION_FIXED_USERNAME, true);
3671 account.setDisplayName(info.second);
3672 createAccount(account);
3673 callback.onAccountCreated(account);
3674 if (Config.X509_VERIFICATION) {
3675 try {
3676 getMemorizingTrustManager()
3677 .getNonInteractive(account.getServer(), null, 0, null)
3678 .checkClientTrusted(chain, "RSA");
3679 } catch (CertificateException e) {
3680 callback.informUser(
3681 R.string.certificate_chain_is_not_trusted);
3682 }
3683 }
3684 } else {
3685 callback.informUser(R.string.account_already_exists);
3686 }
3687 } catch (Exception e) {
3688 callback.informUser(R.string.unable_to_parse_certificate);
3689 }
3690 })
3691 .start();
3692 }
3693
3694 public void updateKeyInAccount(final Account account, final String alias) {
3695 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": update key in account " + alias);
3696 try {
3697 X509Certificate[] chain =
3698 KeyChain.getCertificateChain(XmppConnectionService.this, alias);
3699 Log.d(Config.LOGTAG, account.getJid().asBareJid() + " loaded certificate chain");
3700 Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
3701 if (info == null) {
3702 showErrorToastInUi(R.string.certificate_does_not_contain_jid);
3703 return;
3704 }
3705 if (account.getJid().asBareJid().equals(info.first)) {
3706 account.setPrivateKeyAlias(alias);
3707 account.setDisplayName(info.second);
3708 databaseBackend.updateAccount(account);
3709 if (Config.X509_VERIFICATION) {
3710 try {
3711 getMemorizingTrustManager()
3712 .getNonInteractive()
3713 .checkClientTrusted(chain, "RSA");
3714 } catch (CertificateException e) {
3715 showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
3716 }
3717 account.getAxolotlService().regenerateKeys(true);
3718 }
3719 } else {
3720 showErrorToastInUi(R.string.jid_does_not_match_certificate);
3721 }
3722 } catch (Exception e) {
3723 e.printStackTrace();
3724 }
3725 }
3726
3727 public boolean updateAccount(final Account account) {
3728 if (databaseBackend.updateAccount(account)) {
3729 Integer color = account.getColorToSave();
3730 if (color == null) {
3731 getPreferences().edit().remove("account_color:" + account.getUuid()).commit();
3732 } else {
3733 getPreferences().edit().putInt("account_color:" + account.getUuid(), color.intValue()).commit();
3734 }
3735 account.setShowErrorNotification(true);
3736 this.statusListener.onStatusChanged(account);
3737 databaseBackend.updateAccount(account);
3738 reconnectAccountInBackground(account);
3739 updateAccountUi();
3740 getNotificationService().updateErrorNotification();
3741 toggleForegroundService();
3742 syncEnabledAccountSetting();
3743 mChannelDiscoveryService.cleanCache();
3744 if (CallIntegration.hasSystemFeature(this)) {
3745 CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
3746 }
3747 return true;
3748 } else {
3749 return false;
3750 }
3751 }
3752
3753 public void updateAccountPasswordOnServer(
3754 final Account account,
3755 final String newPassword,
3756 final OnAccountPasswordChanged callback) {
3757 final Iq iq = getIqGenerator().generateSetPassword(account, newPassword);
3758 sendIqPacket(
3759 account,
3760 iq,
3761 (packet) -> {
3762 if (packet.getType() == Iq.Type.RESULT) {
3763 account.setPassword(newPassword);
3764 account.setOption(Account.OPTION_MAGIC_CREATE, false);
3765 databaseBackend.updateAccount(account);
3766 callback.onPasswordChangeSucceeded();
3767 } else {
3768 callback.onPasswordChangeFailed();
3769 }
3770 });
3771 }
3772
3773 public void unregisterAccount(final Account account, final Consumer<Boolean> callback) {
3774 final Iq iqPacket = new Iq(Iq.Type.SET);
3775 final Element query = iqPacket.addChild("query", Namespace.REGISTER);
3776 query.addChild("remove");
3777 sendIqPacket(
3778 account,
3779 iqPacket,
3780 (response) -> {
3781 if (response.getType() == Iq.Type.RESULT) {
3782 deleteAccount(account);
3783 callback.accept(true);
3784 } else {
3785 callback.accept(false);
3786 }
3787 });
3788 }
3789
3790 public void deleteAccount(final Account account) {
3791 getPreferences().edit().remove("onboarding_continued").commit();
3792 final boolean connected = account.getStatus() == Account.State.ONLINE;
3793 synchronized (this.conversations) {
3794 if (connected) {
3795 account.getAxolotlService().deleteOmemoIdentity();
3796 }
3797 for (final Conversation conversation : conversations) {
3798 if (conversation.getAccount() == account) {
3799 if (conversation.getMode() == Conversation.MODE_MULTI) {
3800 if (connected) {
3801 leaveMuc(conversation);
3802 }
3803 }
3804 conversations.remove(conversation);
3805 mNotificationService.clear(conversation);
3806 }
3807 }
3808 new Thread(() -> {
3809 for (final Contact contact : account.getRoster().getContacts()) {
3810 contact.unregisterAsPhoneAccount(this);
3811 }
3812 }).start();
3813 if (account.getXmppConnection() != null) {
3814 new Thread(() -> disconnect(account, !connected)).start();
3815 }
3816 final Runnable runnable =
3817 () -> {
3818 if (!databaseBackend.deleteAccount(account)) {
3819 Log.d(
3820 Config.LOGTAG,
3821 account.getJid().asBareJid() + ": unable to delete account");
3822 }
3823 };
3824 mDatabaseWriterExecutor.execute(runnable);
3825 this.accounts.remove(account);
3826 if (CallIntegration.hasSystemFeature(this)) {
3827 CallIntegrationConnectionService.unregisterPhoneAccount(this, account);
3828 }
3829 this.mRosterSyncTaskManager.clear(account);
3830 updateAccountUi();
3831 mNotificationService.updateErrorNotification();
3832 syncEnabledAccountSetting();
3833 toggleForegroundService();
3834 }
3835 }
3836
3837 public void setOnConversationListChangedListener(OnConversationUpdate listener) {
3838 final boolean remainingListeners;
3839 synchronized (LISTENER_LOCK) {
3840 remainingListeners = checkListeners();
3841 if (!this.mOnConversationUpdates.add(listener)) {
3842 Log.w(
3843 Config.LOGTAG,
3844 listener.getClass().getName()
3845 + " is already registered as ConversationListChangedListener");
3846 }
3847 this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
3848 }
3849 if (remainingListeners) {
3850 switchToForeground();
3851 }
3852 }
3853
3854 public void removeOnConversationListChangedListener(OnConversationUpdate listener) {
3855 final boolean remainingListeners;
3856 synchronized (LISTENER_LOCK) {
3857 this.mOnConversationUpdates.remove(listener);
3858 this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
3859 remainingListeners = checkListeners();
3860 }
3861 if (remainingListeners) {
3862 switchToBackground();
3863 }
3864 }
3865
3866 public void setOnShowErrorToastListener(OnShowErrorToast listener) {
3867 final boolean remainingListeners;
3868 synchronized (LISTENER_LOCK) {
3869 remainingListeners = checkListeners();
3870 if (!this.mOnShowErrorToasts.add(listener)) {
3871 Log.w(
3872 Config.LOGTAG,
3873 listener.getClass().getName()
3874 + " is already registered as OnShowErrorToastListener");
3875 }
3876 }
3877 if (remainingListeners) {
3878 switchToForeground();
3879 }
3880 }
3881
3882 public void removeOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
3883 final boolean remainingListeners;
3884 synchronized (LISTENER_LOCK) {
3885 this.mOnShowErrorToasts.remove(onShowErrorToast);
3886 remainingListeners = checkListeners();
3887 }
3888 if (remainingListeners) {
3889 switchToBackground();
3890 }
3891 }
3892
3893 public void setOnAccountListChangedListener(OnAccountUpdate listener) {
3894 final boolean remainingListeners;
3895 synchronized (LISTENER_LOCK) {
3896 remainingListeners = checkListeners();
3897 if (!this.mOnAccountUpdates.add(listener)) {
3898 Log.w(
3899 Config.LOGTAG,
3900 listener.getClass().getName()
3901 + " is already registered as OnAccountListChangedtListener");
3902 }
3903 }
3904 if (remainingListeners) {
3905 switchToForeground();
3906 }
3907 }
3908
3909 public void removeOnAccountListChangedListener(OnAccountUpdate listener) {
3910 final boolean remainingListeners;
3911 synchronized (LISTENER_LOCK) {
3912 this.mOnAccountUpdates.remove(listener);
3913 remainingListeners = checkListeners();
3914 }
3915 if (remainingListeners) {
3916 switchToBackground();
3917 }
3918 }
3919
3920 public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
3921 final boolean remainingListeners;
3922 synchronized (LISTENER_LOCK) {
3923 remainingListeners = checkListeners();
3924 if (!this.mOnCaptchaRequested.add(listener)) {
3925 Log.w(
3926 Config.LOGTAG,
3927 listener.getClass().getName()
3928 + " is already registered as OnCaptchaRequestListener");
3929 }
3930 }
3931 if (remainingListeners) {
3932 switchToForeground();
3933 }
3934 }
3935
3936 public void removeOnCaptchaRequestedListener(OnCaptchaRequested listener) {
3937 final boolean remainingListeners;
3938 synchronized (LISTENER_LOCK) {
3939 this.mOnCaptchaRequested.remove(listener);
3940 remainingListeners = checkListeners();
3941 }
3942 if (remainingListeners) {
3943 switchToBackground();
3944 }
3945 }
3946
3947 public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
3948 final boolean remainingListeners;
3949 synchronized (LISTENER_LOCK) {
3950 remainingListeners = checkListeners();
3951 if (!this.mOnRosterUpdates.add(listener)) {
3952 Log.w(
3953 Config.LOGTAG,
3954 listener.getClass().getName()
3955 + " is already registered as OnRosterUpdateListener");
3956 }
3957 }
3958 if (remainingListeners) {
3959 switchToForeground();
3960 }
3961 }
3962
3963 public void removeOnRosterUpdateListener(final OnRosterUpdate listener) {
3964 final boolean remainingListeners;
3965 synchronized (LISTENER_LOCK) {
3966 this.mOnRosterUpdates.remove(listener);
3967 remainingListeners = checkListeners();
3968 }
3969 if (remainingListeners) {
3970 switchToBackground();
3971 }
3972 }
3973
3974 public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
3975 final boolean remainingListeners;
3976 synchronized (LISTENER_LOCK) {
3977 remainingListeners = checkListeners();
3978 if (!this.mOnUpdateBlocklist.add(listener)) {
3979 Log.w(
3980 Config.LOGTAG,
3981 listener.getClass().getName()
3982 + " is already registered as OnUpdateBlocklistListener");
3983 }
3984 }
3985 if (remainingListeners) {
3986 switchToForeground();
3987 }
3988 }
3989
3990 public void removeOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
3991 final boolean remainingListeners;
3992 synchronized (LISTENER_LOCK) {
3993 this.mOnUpdateBlocklist.remove(listener);
3994 remainingListeners = checkListeners();
3995 }
3996 if (remainingListeners) {
3997 switchToBackground();
3998 }
3999 }
4000
4001 public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
4002 final boolean remainingListeners;
4003 synchronized (LISTENER_LOCK) {
4004 remainingListeners = checkListeners();
4005 if (!this.mOnKeyStatusUpdated.add(listener)) {
4006 Log.w(
4007 Config.LOGTAG,
4008 listener.getClass().getName()
4009 + " is already registered as OnKeyStatusUpdateListener");
4010 }
4011 }
4012 if (remainingListeners) {
4013 switchToForeground();
4014 }
4015 }
4016
4017 public void removeOnNewKeysAvailableListener(final OnKeyStatusUpdated listener) {
4018 final boolean remainingListeners;
4019 synchronized (LISTENER_LOCK) {
4020 this.mOnKeyStatusUpdated.remove(listener);
4021 remainingListeners = checkListeners();
4022 }
4023 if (remainingListeners) {
4024 switchToBackground();
4025 }
4026 }
4027
4028 public void setOnRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
4029 final boolean remainingListeners;
4030 synchronized (LISTENER_LOCK) {
4031 remainingListeners = checkListeners();
4032 if (!this.onJingleRtpConnectionUpdate.add(listener)) {
4033 Log.w(
4034 Config.LOGTAG,
4035 listener.getClass().getName()
4036 + " is already registered as OnJingleRtpConnectionUpdate");
4037 }
4038 }
4039 if (remainingListeners) {
4040 switchToForeground();
4041 }
4042 }
4043
4044 public void removeRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
4045 final boolean remainingListeners;
4046 synchronized (LISTENER_LOCK) {
4047 this.onJingleRtpConnectionUpdate.remove(listener);
4048 remainingListeners = checkListeners();
4049 }
4050 if (remainingListeners) {
4051 switchToBackground();
4052 }
4053 }
4054
4055 public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
4056 final boolean remainingListeners;
4057 synchronized (LISTENER_LOCK) {
4058 remainingListeners = checkListeners();
4059 if (!this.mOnMucRosterUpdate.add(listener)) {
4060 Log.w(
4061 Config.LOGTAG,
4062 listener.getClass().getName()
4063 + " is already registered as OnMucRosterListener");
4064 }
4065 }
4066 if (remainingListeners) {
4067 switchToForeground();
4068 }
4069 }
4070
4071 public void removeOnMucRosterUpdateListener(final OnMucRosterUpdate listener) {
4072 final boolean remainingListeners;
4073 synchronized (LISTENER_LOCK) {
4074 this.mOnMucRosterUpdate.remove(listener);
4075 remainingListeners = checkListeners();
4076 }
4077 if (remainingListeners) {
4078 switchToBackground();
4079 }
4080 }
4081
4082 public boolean checkListeners() {
4083 return (this.mOnAccountUpdates.isEmpty()
4084 && this.mOnConversationUpdates.isEmpty()
4085 && this.mOnRosterUpdates.isEmpty()
4086 && this.mOnCaptchaRequested.isEmpty()
4087 && this.mOnMucRosterUpdate.isEmpty()
4088 && this.mOnUpdateBlocklist.isEmpty()
4089 && this.mOnShowErrorToasts.isEmpty()
4090 && this.onJingleRtpConnectionUpdate.isEmpty()
4091 && this.mOnKeyStatusUpdated.isEmpty());
4092 }
4093
4094 private void switchToForeground() {
4095 toggleSoftDisabled(false);
4096 final boolean broadcastLastActivity = broadcastLastActivity();
4097 for (Conversation conversation : getConversations()) {
4098 if (conversation.getMode() == Conversation.MODE_MULTI) {
4099 conversation.getMucOptions().resetChatState();
4100 } else {
4101 conversation.setIncomingChatState(Config.DEFAULT_CHAT_STATE);
4102 }
4103 }
4104 for (Account account : getAccounts()) {
4105 if (account.getStatus() == Account.State.ONLINE) {
4106 account.deactivateGracePeriod();
4107 final XmppConnection connection = account.getXmppConnection();
4108 if (connection != null) {
4109 if (connection.getFeatures().csi()) {
4110 connection.sendActive();
4111 }
4112 if (broadcastLastActivity) {
4113 sendPresence(
4114 account,
4115 false); // send new presence but don't include idle because we are
4116 // not
4117 }
4118 }
4119 }
4120 }
4121 Log.d(Config.LOGTAG, "app switched into foreground");
4122 }
4123
4124 private void switchToBackground() {
4125 final boolean broadcastLastActivity = broadcastLastActivity();
4126 if (broadcastLastActivity) {
4127 mLastActivity = System.currentTimeMillis();
4128 final SharedPreferences.Editor editor = getPreferences().edit();
4129 editor.putLong(SETTING_LAST_ACTIVITY_TS, mLastActivity);
4130 editor.apply();
4131 }
4132 for (Account account : getAccounts()) {
4133 if (account.getStatus() == Account.State.ONLINE) {
4134 XmppConnection connection = account.getXmppConnection();
4135 if (connection != null) {
4136 if (broadcastLastActivity) {
4137 sendPresence(account, true);
4138 }
4139 if (connection.getFeatures().csi()) {
4140 connection.sendInactive();
4141 }
4142 }
4143 }
4144 }
4145 this.mNotificationService.setIsInForeground(false);
4146 Log.d(Config.LOGTAG, "app switched into background");
4147 }
4148
4149 public void connectMultiModeConversations(Account account) {
4150 List<Conversation> conversations = getConversations();
4151 for (Conversation conversation : conversations) {
4152 if (conversation.getMode() == Conversation.MODE_MULTI
4153 && conversation.getAccount() == account) {
4154 joinMuc(conversation);
4155 }
4156 }
4157 }
4158
4159 public void mucSelfPingAndRejoin(final Conversation conversation) {
4160 final Account account = conversation.getAccount();
4161 synchronized (account.inProgressConferenceJoins) {
4162 if (account.inProgressConferenceJoins.contains(conversation)) {
4163 Log.d(
4164 Config.LOGTAG,
4165 account.getJid().asBareJid()
4166 + ": canceling muc self ping because join is already under way");
4167 return;
4168 }
4169 }
4170 synchronized (account.inProgressConferencePings) {
4171 if (!account.inProgressConferencePings.add(conversation)) {
4172 Log.d(
4173 Config.LOGTAG,
4174 account.getJid().asBareJid()
4175 + ": canceling muc self ping because ping is already under way");
4176 return;
4177 }
4178 }
4179 // TODO use PingManager
4180 final Jid self = conversation.getMucOptions().getSelf().getFullJid();
4181 final Iq ping = new Iq(Iq.Type.GET);
4182 ping.setTo(self);
4183 ping.addChild("ping", Namespace.PING);
4184 sendIqPacket(
4185 conversation.getAccount(),
4186 ping,
4187 (response) -> {
4188 if (response.getType() == Iq.Type.ERROR) {
4189 final var error = response.getError();
4190 if (error == null
4191 || error.hasChild("service-unavailable")
4192 || error.hasChild("feature-not-implemented")
4193 || error.hasChild("item-not-found")) {
4194 Log.d(
4195 Config.LOGTAG,
4196 account.getJid().asBareJid()
4197 + ": ping to "
4198 + self
4199 + " came back as ignorable error");
4200 } else {
4201 Log.d(
4202 Config.LOGTAG,
4203 account.getJid().asBareJid()
4204 + ": ping to "
4205 + self
4206 + " failed. attempting rejoin");
4207 joinMuc(conversation);
4208 }
4209 } else if (response.getType() == Iq.Type.RESULT) {
4210 Log.d(
4211 Config.LOGTAG,
4212 account.getJid().asBareJid()
4213 + ": ping to "
4214 + self
4215 + " came back fine");
4216 }
4217 synchronized (account.inProgressConferencePings) {
4218 account.inProgressConferencePings.remove(conversation);
4219 }
4220 });
4221 }
4222
4223 public void joinMuc(Conversation conversation) {
4224 joinMuc(conversation, null, false);
4225 }
4226
4227 public void joinMuc(Conversation conversation, boolean followedInvite) {
4228 joinMuc(conversation, null, followedInvite);
4229 }
4230
4231 private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
4232 joinMuc(conversation, onConferenceJoined, false);
4233 }
4234
4235 private void joinMuc(
4236 final Conversation conversation,
4237 final OnConferenceJoined onConferenceJoined,
4238 final boolean followedInvite) {
4239 final Account account = conversation.getAccount();
4240 synchronized (account.pendingConferenceJoins) {
4241 account.pendingConferenceJoins.remove(conversation);
4242 }
4243 synchronized (account.pendingConferenceLeaves) {
4244 account.pendingConferenceLeaves.remove(conversation);
4245 }
4246 if (account.getStatus() == Account.State.ONLINE) {
4247 synchronized (account.inProgressConferenceJoins) {
4248 account.inProgressConferenceJoins.add(conversation);
4249 }
4250 if (Config.MUC_LEAVE_BEFORE_JOIN) {
4251 sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions()));
4252 }
4253 conversation.resetMucOptions();
4254 if (onConferenceJoined != null) {
4255 conversation.getMucOptions().flagNoAutoPushConfiguration();
4256 }
4257 conversation.setHasMessagesLeftOnServer(false);
4258 fetchConferenceConfiguration(
4259 conversation,
4260 new OnConferenceConfigurationFetched() {
4261
4262 private void join(Conversation conversation) {
4263 Account account = conversation.getAccount();
4264 final MucOptions mucOptions = conversation.getMucOptions();
4265
4266 if (mucOptions.nonanonymous()
4267 && !mucOptions.membersOnly()
4268 && !conversation.getBooleanAttribute(
4269 "accept_non_anonymous", false)) {
4270 synchronized (account.inProgressConferenceJoins) {
4271 account.inProgressConferenceJoins.remove(conversation);
4272 }
4273 mucOptions.setError(MucOptions.Error.NON_ANONYMOUS);
4274 updateConversationUi();
4275 if (onConferenceJoined != null) {
4276 onConferenceJoined.onConferenceJoined(conversation);
4277 }
4278 return;
4279 }
4280
4281 final Jid joinJid = mucOptions.getSelf().getFullJid();
4282 Log.d(
4283 Config.LOGTAG,
4284 account.getJid().asBareJid().toString()
4285 + ": joining conversation "
4286 + joinJid.toString());
4287 final var packet =
4288 mPresenceGenerator.selfPresence(
4289 account,
4290 im.conversations.android.xmpp.model.stanza.Presence
4291 .Availability.ONLINE,
4292 mucOptions.nonanonymous()
4293 || onConferenceJoined != null,
4294 mucOptions.getSelf().getNick());
4295 packet.setTo(joinJid);
4296 Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
4297 if (conversation.getMucOptions().getPassword() != null) {
4298 x.addChild("password").setContent(mucOptions.getPassword());
4299 }
4300
4301 if (mucOptions.mamSupport()) {
4302 // Use MAM instead of the limited muc history to get history
4303 x.addChild("history").setAttribute("maxchars", "0");
4304 } else {
4305 // Fallback to muc history
4306 x.addChild("history")
4307 .setAttribute(
4308 "since",
4309 PresenceGenerator.getTimestamp(
4310 conversation
4311 .getLastMessageTransmitted()
4312 .getTimestamp()));
4313 }
4314 sendPresencePacket(account, packet);
4315 if (onConferenceJoined != null) {
4316 onConferenceJoined.onConferenceJoined(conversation);
4317 }
4318 if (!joinJid.equals(conversation.getJid())) {
4319 conversation.setContactJid(joinJid);
4320 databaseBackend.updateConversation(conversation);
4321 }
4322
4323 maybeRegisterWithMuc(conversation, null);
4324
4325 if (mucOptions.mamSupport()) {
4326 getMessageArchiveService().catchupMUC(conversation);
4327 }
4328 fetchConferenceMembers(conversation);
4329 if (mucOptions.isPrivateAndNonAnonymous()) {
4330 if (followedInvite) {
4331 final Bookmark bookmark = conversation.getBookmark();
4332 if (bookmark != null) {
4333 if (!bookmark.autojoin()) {
4334 bookmark.setAutojoin(true);
4335 createBookmark(account, bookmark);
4336 }
4337 } else {
4338 saveConversationAsBookmark(conversation, null);
4339 }
4340 }
4341 }
4342 synchronized (account.inProgressConferenceJoins) {
4343 account.inProgressConferenceJoins.remove(conversation);
4344 sendUnsentMessages(conversation);
4345 }
4346 }
4347
4348 @Override
4349 public void onConferenceConfigurationFetched(Conversation conversation) {
4350 if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
4351 Log.d(
4352 Config.LOGTAG,
4353 account.getJid().asBareJid()
4354 + ": conversation ("
4355 + conversation.getJid()
4356 + ") got archived before IQ result");
4357 return;
4358 }
4359 join(conversation);
4360 }
4361
4362 @Override
4363 public void onFetchFailed(
4364 final Conversation conversation, final String errorCondition) {
4365 if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
4366 Log.d(
4367 Config.LOGTAG,
4368 account.getJid().asBareJid()
4369 + ": conversation ("
4370 + conversation.getJid()
4371 + ") got archived before IQ result");
4372 return;
4373 }
4374 if ("remote-server-not-found".equals(errorCondition)) {
4375 synchronized (account.inProgressConferenceJoins) {
4376 account.inProgressConferenceJoins.remove(conversation);
4377 }
4378 conversation
4379 .getMucOptions()
4380 .setError(MucOptions.Error.SERVER_NOT_FOUND);
4381 updateConversationUi();
4382 } else {
4383 join(conversation);
4384 fetchConferenceConfiguration(conversation);
4385 }
4386 }
4387 });
4388 updateConversationUi();
4389 } else {
4390 synchronized (account.pendingConferenceJoins) {
4391 account.pendingConferenceJoins.add(conversation);
4392 }
4393 conversation.resetMucOptions();
4394 conversation.setHasMessagesLeftOnServer(false);
4395 updateConversationUi();
4396 }
4397 }
4398
4399 private void fetchConferenceMembers(final Conversation conversation) {
4400 final Account account = conversation.getAccount();
4401 final AxolotlService axolotlService = account.getAxolotlService();
4402 final var affiliations = new ArrayList<String>();
4403 affiliations.add("outcast");
4404 if (conversation.getMucOptions().isPrivateAndNonAnonymous()) affiliations.addAll(List.of("member", "admin", "owner"));
4405 final Consumer<Iq> callback =
4406 new Consumer<Iq>() {
4407
4408 private int i = 0;
4409 private boolean success = true;
4410
4411 @Override
4412 public void accept(Iq response) {
4413 final boolean omemoEnabled =
4414 conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL;
4415 Element query = response.query("http://jabber.org/protocol/muc#admin");
4416 if (response.getType() == Iq.Type.RESULT && query != null) {
4417 for (Element child : query.getChildren()) {
4418 if ("item".equals(child.getName())) {
4419 MucOptions.User user =
4420 AbstractParser.parseItem(conversation, child);
4421 user.setOnline(false);
4422 if (!user.realJidMatchesAccount()) {
4423 boolean isNew =
4424 conversation.getMucOptions().updateUser(user);
4425 Contact contact = user.getContact();
4426 if (omemoEnabled
4427 && isNew
4428 && user.getRealJid() != null
4429 && (contact == null
4430 || !contact.mutualPresenceSubscription())
4431 && axolotlService.hasEmptyDeviceList(
4432 user.getRealJid())) {
4433 axolotlService.fetchDeviceIds(user.getRealJid());
4434 }
4435 }
4436 }
4437 }
4438 } else {
4439 success = false;
4440 Log.d(
4441 Config.LOGTAG,
4442 account.getJid().asBareJid()
4443 + ": could not request affiliation "
4444 + affiliations.get(i)
4445 + " in "
4446 + conversation.getJid().asBareJid());
4447 }
4448 ++i;
4449 if (i >= affiliations.size()) {
4450 final var mucOptions = conversation.getMucOptions();
4451 List<Jid> members = mucOptions.getMembers(true);
4452 if (success) {
4453 List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
4454 boolean changed = false;
4455 for (ListIterator<Jid> iterator = cryptoTargets.listIterator();
4456 iterator.hasNext(); ) {
4457 Jid jid = iterator.next();
4458 if (!members.contains(jid)
4459 && !members.contains(jid.getDomain())) {
4460 iterator.remove();
4461 Log.d(
4462 Config.LOGTAG,
4463 account.getJid().asBareJid()
4464 + ": removed "
4465 + jid
4466 + " from crypto targets of "
4467 + conversation.getName());
4468 changed = true;
4469 }
4470 }
4471 if (changed) {
4472 conversation.setAcceptedCryptoTargets(cryptoTargets);
4473 updateConversation(conversation);
4474 }
4475 }
4476 getAvatarService().clear(mucOptions);
4477 updateMucRosterUi();
4478 updateConversationUi();
4479 }
4480 }
4481 };
4482 for (String affiliation : affiliations) {
4483 sendIqPacket(
4484 account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
4485 }
4486 Log.d(
4487 Config.LOGTAG,
4488 account.getJid().asBareJid() + ": fetching members for " + conversation.getName());
4489 }
4490
4491 public void providePasswordForMuc(final Conversation conversation, final String password) {
4492 if (conversation.getMode() == Conversation.MODE_MULTI) {
4493 conversation.getMucOptions().setPassword(password);
4494 if (conversation.getBookmark() != null) {
4495 final Bookmark bookmark = conversation.getBookmark();
4496 bookmark.setAutojoin(true);
4497 createBookmark(conversation.getAccount(), bookmark);
4498 }
4499 updateConversation(conversation);
4500 joinMuc(conversation);
4501 }
4502 }
4503
4504 public void deleteAvatar(final Account account) {
4505 final AtomicBoolean executed = new AtomicBoolean(false);
4506 final Runnable onDeleted =
4507 () -> {
4508 if (executed.compareAndSet(false, true)) {
4509 account.setAvatar(null);
4510 databaseBackend.updateAccount(account);
4511 getAvatarService().clear(account);
4512 updateAccountUi();
4513 }
4514 };
4515 deleteVcardAvatar(account, onDeleted);
4516 deletePepNode(account, Namespace.AVATAR_DATA);
4517 deletePepNode(account, Namespace.AVATAR_METADATA, onDeleted);
4518 }
4519
4520 public void deletePepNode(final Account account, final String node) {
4521 deletePepNode(account, node, null);
4522 }
4523
4524 private void deletePepNode(final Account account, final String node, final Runnable runnable) {
4525 final Iq request = mIqGenerator.deleteNode(node);
4526 sendIqPacket(
4527 account,
4528 request,
4529 (packet) -> {
4530 if (packet.getType() == Iq.Type.RESULT) {
4531 Log.d(
4532 Config.LOGTAG,
4533 account.getJid().asBareJid()
4534 + ": successfully deleted pep node "
4535 + node);
4536 if (runnable != null) {
4537 runnable.run();
4538 }
4539 } else {
4540 Log.d(
4541 Config.LOGTAG,
4542 account.getJid().asBareJid() + ": failed to delete " + packet);
4543 }
4544 });
4545 }
4546
4547 private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) {
4548 final Iq retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid());
4549 sendIqPacket(
4550 account,
4551 retrieveVcard,
4552 (response) -> {
4553 if (response.getType() != Iq.Type.RESULT) {
4554 Log.d(
4555 Config.LOGTAG,
4556 account.getJid().asBareJid() + ": no vCard set. nothing to do");
4557 return;
4558 }
4559 final Element vcard = response.findChild("vCard", "vcard-temp");
4560 if (vcard == null) {
4561 Log.d(
4562 Config.LOGTAG,
4563 account.getJid().asBareJid() + ": no vCard set. nothing to do");
4564 return;
4565 }
4566 Element photo = vcard.findChild("PHOTO");
4567 if (photo == null) {
4568 photo = vcard.addChild("PHOTO");
4569 }
4570 photo.clearChildren();
4571 final Iq publication = new Iq(Iq.Type.SET);
4572 publication.setTo(account.getJid().asBareJid());
4573 publication.addChild(vcard);
4574 sendIqPacket(
4575 account,
4576 publication,
4577 (publicationResponse) -> {
4578 if (publicationResponse.getType() == Iq.Type.RESULT) {
4579 Log.d(
4580 Config.LOGTAG,
4581 account.getJid().asBareJid()
4582 + ": successfully deleted vcard avatar");
4583 runnable.run();
4584 } else {
4585 Log.d(
4586 Config.LOGTAG,
4587 "failed to publish vcard "
4588 + publicationResponse.getErrorCondition());
4589 }
4590 });
4591 });
4592 }
4593
4594 private boolean hasEnabledAccounts() {
4595 if (this.accounts == null) {
4596 return false;
4597 }
4598 for (final Account account : this.accounts) {
4599 if (account.isConnectionEnabled()) {
4600 return true;
4601 }
4602 }
4603 return false;
4604 }
4605
4606 public void getAttachments(
4607 final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) {
4608 getAttachments(
4609 conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded);
4610 }
4611
4612 public void getAttachments(
4613 final Account account,
4614 final Jid jid,
4615 final int limit,
4616 final OnMediaLoaded onMediaLoaded) {
4617 getAttachments(account.getUuid(), jid.asBareJid(), limit, onMediaLoaded);
4618 }
4619
4620 public void getAttachments(
4621 final String account,
4622 final Jid jid,
4623 final int limit,
4624 final OnMediaLoaded onMediaLoaded) {
4625 new Thread(
4626 () ->
4627 onMediaLoaded.onMediaLoaded(
4628 fileBackend.convertToAttachments(
4629 databaseBackend.getRelativeFilePaths(
4630 account, jid, limit))))
4631 .start();
4632 }
4633
4634 public void persistSelfNick(final MucOptions.User self, final boolean modified) {
4635 final Conversation conversation = self.getConversation();
4636 final Account account = conversation.getAccount();
4637 final Jid full = self.getFullJid();
4638 if (!full.equals(conversation.getJid())) {
4639 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persisting full jid " + full);
4640 conversation.setContactJid(full);
4641 databaseBackend.updateConversation(conversation);
4642 }
4643
4644 final String nick = self.getNick();
4645 final Bookmark bookmark = conversation.getBookmark();
4646 if (bookmark == null || !modified) {
4647 return;
4648 }
4649 final String defaultNick = MucOptions.defaultNick(account);
4650 if (nick.equals(defaultNick) || nick.equals(bookmark.getNick())) {
4651 return;
4652 }
4653 Log.d(
4654 Config.LOGTAG,
4655 account.getJid().asBareJid()
4656 + ": persist nick '"
4657 + full.getResource()
4658 + "' into bookmark for "
4659 + conversation.getJid().asBareJid());
4660 bookmark.setNick(nick);
4661 createBookmark(bookmark.getAccount(), bookmark);
4662 }
4663
4664 public void presenceToMuc(final Conversation conversation) {
4665 final MucOptions options = conversation.getMucOptions();
4666 if (options.online()) {
4667 Account account = conversation.getAccount();
4668 final Jid joinJid = options.getSelf().getFullJid();
4669 final var packet = mPresenceGenerator.selfPresence(account, Presence.Availability.ONLINE, options.nonanonymous(), options.getSelf().getNick());
4670 packet.setTo(joinJid);
4671 sendPresencePacket(account, packet);
4672 }
4673 }
4674
4675 public boolean renameInMuc(
4676 final Conversation conversation,
4677 final String nick,
4678 final UiCallback<Conversation> callback) {
4679 final Account account = conversation.getAccount();
4680 final Bookmark bookmark = conversation.getBookmark();
4681 final MucOptions options = conversation.getMucOptions();
4682 final Jid joinJid = options.createJoinJid(nick);
4683 if (joinJid == null) {
4684 return false;
4685 }
4686 if (options.online()) {
4687 maybeRegisterWithMuc(conversation, nick);
4688 options.setOnRenameListener(
4689 new OnRenameListener() {
4690
4691 @Override
4692 public void onSuccess() {
4693 final var packet = mPresenceGenerator.selfPresence(account, Presence.Availability.ONLINE, options.nonanonymous(), nick);
4694 packet.setTo(joinJid);
4695 sendPresencePacket(account, packet);
4696 callback.success(conversation);
4697 }
4698
4699 @Override
4700 public void onFailure() {
4701 callback.error(R.string.nick_in_use, conversation);
4702 }
4703 });
4704
4705 final var packet =
4706 mPresenceGenerator.selfPresence(
4707 account,
4708 im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
4709 options.nonanonymous(), nick);
4710 packet.setTo(joinJid);
4711 sendPresencePacket(account, packet);
4712 if (nick.equals(MucOptions.defaultNick(account))
4713 && bookmark != null
4714 && bookmark.getNick() != null) {
4715 Log.d(
4716 Config.LOGTAG,
4717 account.getJid().asBareJid()
4718 + ": removing nick from bookmark for "
4719 + bookmark.getJid());
4720 bookmark.setNick(null);
4721 createBookmark(account, bookmark);
4722 }
4723 } else {
4724 conversation.setContactJid(joinJid);
4725 databaseBackend.updateConversation(conversation);
4726 if (account.getStatus() == Account.State.ONLINE) {
4727 if (bookmark != null) {
4728 bookmark.setNick(nick);
4729 createBookmark(account, bookmark);
4730 }
4731 joinMuc(conversation);
4732 }
4733 }
4734 return true;
4735 }
4736
4737 public void checkMucRequiresRename() {
4738 synchronized (this.conversations) {
4739 for (final Conversation conversation : this.conversations) {
4740 if (conversation.getMode() == Conversational.MODE_MULTI) {
4741 checkMucRequiresRename(conversation);
4742 }
4743 }
4744 }
4745 }
4746
4747 private void checkMucRequiresRename(final Conversation conversation) {
4748 final var options = conversation.getMucOptions();
4749 if (!options.online()) {
4750 return;
4751 }
4752 final var account = conversation.getAccount();
4753 final String current = options.getActualNick();
4754 final String proposed = options.getProposedNickPure();
4755 if (current == null || current.equals(proposed)) {
4756 return;
4757 }
4758 final Jid joinJid = options.createJoinJid(proposed);
4759 Log.d(
4760 Config.LOGTAG,
4761 String.format(
4762 "%s: muc rename required %s (was: %s)",
4763 account.getJid().asBareJid(), joinJid, current));
4764 final var packet =
4765 mPresenceGenerator.selfPresence(
4766 account,
4767 im.conversations.android.xmpp.model.stanza.Presence.Availability.ONLINE,
4768 options.nonanonymous(), proposed);
4769 packet.setTo(joinJid);
4770 sendPresencePacket(account, packet);
4771 }
4772
4773 public void leaveMuc(Conversation conversation) {
4774 leaveMuc(conversation, false);
4775 }
4776
4777 private void leaveMuc(Conversation conversation, boolean now) {
4778 final Account account = conversation.getAccount();
4779 synchronized (account.pendingConferenceJoins) {
4780 account.pendingConferenceJoins.remove(conversation);
4781 }
4782 synchronized (account.pendingConferenceLeaves) {
4783 account.pendingConferenceLeaves.remove(conversation);
4784 }
4785 if (account.getStatus() == Account.State.ONLINE || now) {
4786 sendPresencePacket(
4787 conversation.getAccount(),
4788 mPresenceGenerator.leave(conversation.getMucOptions()));
4789 conversation.getMucOptions().setOffline();
4790 Bookmark bookmark = conversation.getBookmark();
4791 if (bookmark != null) {
4792 bookmark.setConversation(null);
4793 }
4794 Log.d(
4795 Config.LOGTAG,
4796 conversation.getAccount().getJid().asBareJid()
4797 + ": leaving muc "
4798 + conversation.getJid());
4799 final var connection = account.getXmppConnection();
4800 if (connection != null) {
4801 connection.getManager(DiscoManager.class).clear(conversation.getJid().asBareJid());
4802 }
4803 } else {
4804 synchronized (account.pendingConferenceLeaves) {
4805 account.pendingConferenceLeaves.add(conversation);
4806 }
4807 }
4808 }
4809
4810 public String findConferenceServer(final Account account) {
4811 String server;
4812 if (account.getXmppConnection() != null) {
4813 server = account.getXmppConnection().getMucServer();
4814 if (server != null) {
4815 return server;
4816 }
4817 }
4818 for (Account other : getAccounts()) {
4819 if (other != account && other.getXmppConnection() != null) {
4820 server = other.getXmppConnection().getMucServer();
4821 if (server != null) {
4822 return server;
4823 }
4824 }
4825 }
4826 return null;
4827 }
4828
4829 public void createPublicChannel(
4830 final Account account,
4831 final String name,
4832 final Jid address,
4833 final UiCallback<Conversation> callback) {
4834 joinMuc(
4835 findOrCreateConversation(account, address, true, false, true),
4836 conversation -> {
4837 final Bundle configuration = IqGenerator.defaultChannelConfiguration();
4838 if (!TextUtils.isEmpty(name)) {
4839 configuration.putString("muc#roomconfig_roomname", name);
4840 }
4841 pushConferenceConfiguration(
4842 conversation,
4843 configuration,
4844 new OnConfigurationPushed() {
4845 @Override
4846 public void onPushSucceeded() {
4847 saveConversationAsBookmark(conversation, name);
4848 callback.success(conversation);
4849 }
4850
4851 @Override
4852 public void onPushFailed() {
4853 if (conversation
4854 .getMucOptions()
4855 .getSelf()
4856 .getAffiliation()
4857 .ranks(MucOptions.Affiliation.OWNER)) {
4858 callback.error(
4859 R.string.unable_to_set_channel_configuration,
4860 conversation);
4861 } else {
4862 callback.error(
4863 R.string.joined_an_existing_channel, conversation);
4864 }
4865 }
4866 });
4867 });
4868 }
4869
4870 public boolean createAdhocConference(
4871 final Account account,
4872 final String name,
4873 final Iterable<Jid> jids,
4874 final UiCallback<Conversation> callback) {
4875 Log.d(
4876 Config.LOGTAG,
4877 account.getJid().asBareJid().toString()
4878 + ": creating adhoc conference with "
4879 + jids.toString());
4880 if (account.getStatus() == Account.State.ONLINE) {
4881 try {
4882 String server = findConferenceServer(account);
4883 if (server == null) {
4884 if (callback != null) {
4885 callback.error(R.string.no_conference_server_found, null);
4886 }
4887 return false;
4888 }
4889 final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null);
4890 final Conversation conversation =
4891 findOrCreateConversation(account, jid, true, false, true);
4892 joinMuc(
4893 conversation,
4894 new OnConferenceJoined() {
4895 @Override
4896 public void onConferenceJoined(final Conversation conversation) {
4897 final Bundle configuration =
4898 IqGenerator.defaultGroupChatConfiguration();
4899 if (!TextUtils.isEmpty(name)) {
4900 configuration.putString("muc#roomconfig_roomname", name);
4901 }
4902 pushConferenceConfiguration(
4903 conversation,
4904 configuration,
4905 new OnConfigurationPushed() {
4906 @Override
4907 public void onPushSucceeded() {
4908 for (Jid invite : jids) {
4909 invite(conversation, invite);
4910 }
4911 for (String resource :
4912 account.getSelfContact()
4913 .getPresences()
4914 .toResourceArray()) {
4915 Jid other =
4916 account.getJid().withResource(resource);
4917 Log.d(
4918 Config.LOGTAG,
4919 account.getJid().asBareJid()
4920 + ": sending direct invite to "
4921 + other);
4922 directInvite(conversation, other);
4923 }
4924 saveConversationAsBookmark(conversation, name);
4925 if (callback != null) {
4926 callback.success(conversation);
4927 }
4928 }
4929
4930 @Override
4931 public void onPushFailed() {
4932 archiveConversation(conversation);
4933 if (callback != null) {
4934 callback.error(
4935 R.string.conference_creation_failed,
4936 conversation);
4937 }
4938 }
4939 });
4940 }
4941 });
4942 return true;
4943 } catch (IllegalArgumentException e) {
4944 if (callback != null) {
4945 callback.error(R.string.conference_creation_failed, null);
4946 }
4947 return false;
4948 }
4949 } else {
4950 if (callback != null) {
4951 callback.error(R.string.not_connected_try_again, null);
4952 }
4953 return false;
4954 }
4955 }
4956
4957 public void checkIfMuc(final Account account, final Jid jid, Consumer<Boolean> cb) {
4958 if (jid.isDomainJid()) {
4959 // Spec basically says MUC needs to have a node
4960 // And also specifies that MUC and MUC service should have the same identity...
4961 cb.accept(false);
4962 return;
4963 }
4964
4965 final var connection = account.getXmppConnection();
4966 if (connection == null) {
4967 cb.accept(false); // hmmm...
4968 return;
4969 }
4970 final ListenableFuture<InfoQuery> future =
4971 connection
4972 .getManager(DiscoManager.class)
4973 .info(Entity.discoItem(jid), null);
4974
4975 Futures.addCallback(
4976 future,
4977 new FutureCallback<>() {
4978 @Override
4979 public void onSuccess(InfoQuery result) {
4980 cb.accept(
4981 result.hasFeature("http://jabber.org/protocol/muc") &&
4982 result.hasIdentityWithCategory("conference")
4983 );
4984 }
4985
4986 @Override
4987 public void onFailure(@NonNull Throwable throwable) {
4988 cb.accept(false);
4989 }
4990 },
4991 MoreExecutors.directExecutor()
4992 );
4993 }
4994
4995 public void fetchConferenceConfiguration(final Conversation conversation) {
4996 fetchConferenceConfiguration(conversation, null);
4997 }
4998
4999 public void fetchConferenceConfiguration(
5000 final Conversation conversation, final OnConferenceConfigurationFetched callback) {
5001 final var account = conversation.getAccount();
5002 final var connection = account.getXmppConnection();
5003 if (connection == null) {
5004 return;
5005 }
5006 final var future =
5007 connection
5008 .getManager(DiscoManager.class)
5009 .info(Entity.discoItem(conversation.getJid().asBareJid()), null);
5010 Futures.addCallback(
5011 future,
5012 new FutureCallback<>() {
5013 @Override
5014 public void onSuccess(InfoQuery result) {
5015 final MucOptions mucOptions = conversation.getMucOptions();
5016 final Bookmark bookmark = conversation.getBookmark();
5017 final boolean sameBefore =
5018 StringUtils.equals(
5019 bookmark == null ? null : bookmark.getBookmarkName(),
5020 mucOptions.getName());
5021
5022 final var hadOccupantId = mucOptions.occupantId();
5023 if (mucOptions.updateConfiguration(result)) {
5024 Log.d(
5025 Config.LOGTAG,
5026 account.getJid().asBareJid()
5027 + ": muc configuration changed for "
5028 + conversation.getJid().asBareJid());
5029 updateConversation(conversation);
5030 }
5031
5032 final var hasOccupantId = mucOptions.occupantId();
5033
5034 if (!hadOccupantId && hasOccupantId && mucOptions.online()) {
5035 final var me = mucOptions.getSelf().getFullJid();
5036 Log.d(
5037 Config.LOGTAG,
5038 account.getJid().asBareJid()
5039 + ": gained support for occupant-id in "
5040 + me
5041 + ". resending presence");
5042 final var packet =
5043 mPresenceGenerator.selfPresence(
5044 account,
5045 im.conversations.android.xmpp.model.stanza.Presence
5046 .Availability.ONLINE,
5047 mucOptions.nonanonymous(), mucOptions.getSelf().getNick());
5048 packet.setTo(me);
5049 sendPresencePacket(account, packet);
5050 }
5051
5052 if (bookmark != null
5053 && (sameBefore || bookmark.getBookmarkName() == null)) {
5054 if (bookmark.setBookmarkName(
5055 StringUtils.nullOnEmpty(mucOptions.getName()))) {
5056 createBookmark(account, bookmark);
5057 }
5058 }
5059
5060 if (callback != null) {
5061 callback.onConferenceConfigurationFetched(conversation);
5062 }
5063
5064 updateConversationUi();
5065 }
5066
5067 @Override
5068 public void onFailure(@NonNull Throwable throwable) {
5069 if (throwable instanceof TimeoutException) {
5070 Log.d(
5071 Config.LOGTAG,
5072 account.getJid().asBareJid()
5073 + ": received timeout waiting for conference"
5074 + " configuration fetch");
5075 } else if (throwable
5076 instanceof IqErrorResponseException errorResponseException) {
5077 if (callback != null) {
5078 callback.onFetchFailed(
5079 conversation,
5080 errorResponseException.getResponse().getErrorCondition());
5081 }
5082 }
5083 }
5084 },
5085 MoreExecutors.directExecutor());
5086 }
5087
5088 public void pushNodeConfiguration(
5089 Account account,
5090 final String node,
5091 final Bundle options,
5092 final OnConfigurationPushed callback) {
5093 pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback);
5094 }
5095
5096 public void pushNodeConfiguration(
5097 Account account,
5098 final Jid jid,
5099 final String node,
5100 final Bundle options,
5101 final OnConfigurationPushed callback) {
5102 Log.d(Config.LOGTAG, "pushing node configuration");
5103 sendIqPacket(
5104 account,
5105 mIqGenerator.requestPubsubConfiguration(jid, node),
5106 responseToRequest -> {
5107 if (responseToRequest.getType() == Iq.Type.RESULT) {
5108 Element pubsub =
5109 responseToRequest.findChild(
5110 "pubsub", "http://jabber.org/protocol/pubsub#owner");
5111 Element configuration =
5112 pubsub == null ? null : pubsub.findChild("configure");
5113 Element x =
5114 configuration == null
5115 ? null
5116 : configuration.findChild("x", Namespace.DATA);
5117 if (x != null) {
5118 final Data data = Data.parse(x);
5119 data.submit(options);
5120 sendIqPacket(
5121 account,
5122 mIqGenerator.publishPubsubConfiguration(jid, node, data),
5123 responseToPublish -> {
5124 if (responseToPublish.getType() == Iq.Type.RESULT
5125 && callback != null) {
5126 Log.d(
5127 Config.LOGTAG,
5128 account.getJid().asBareJid()
5129 + ": successfully changed node"
5130 + " configuration for node "
5131 + node);
5132 callback.onPushSucceeded();
5133 } else if (responseToPublish.getType() == Iq.Type.ERROR
5134 && callback != null) {
5135 callback.onPushFailed();
5136 }
5137 });
5138 } else if (callback != null) {
5139 callback.onPushFailed();
5140 }
5141 } else if (responseToRequest.getType() == Iq.Type.ERROR && callback != null) {
5142 callback.onPushFailed();
5143 }
5144 });
5145 }
5146
5147 public void pushConferenceConfiguration(
5148 final Conversation conversation,
5149 final Bundle options,
5150 final OnConfigurationPushed callback) {
5151 if (options.getString("muc#roomconfig_whois", "moderators").equals("anyone")) {
5152 conversation.setAttribute("accept_non_anonymous", true);
5153 updateConversation(conversation);
5154 }
5155 if (options.containsKey("muc#roomconfig_moderatedroom")) {
5156 final boolean moderated = "1".equals(options.getString("muc#roomconfig_moderatedroom"));
5157 options.putString("members_by_default", moderated ? "0" : "1");
5158 }
5159 if (options.containsKey("muc#roomconfig_allowpm")) {
5160 // ejabberd :-/
5161 final boolean allow = "anyone".equals(options.getString("muc#roomconfig_allowpm"));
5162 options.putString("allow_private_messages", allow ? "1" : "0");
5163 options.putString("allow_private_messages_from_visitors", allow ? "anyone" : "nobody");
5164 }
5165 final var account = conversation.getAccount();
5166 final Iq request = new Iq(Iq.Type.GET);
5167 request.setTo(conversation.getJid().asBareJid());
5168 request.query("http://jabber.org/protocol/muc#owner");
5169 sendIqPacket(
5170 account,
5171 request,
5172 response -> {
5173 if (response.getType() == Iq.Type.RESULT) {
5174 final Data data =
5175 Data.parse(response.query().findChild("x", Namespace.DATA));
5176 data.submit(options);
5177 final Iq set = new Iq(Iq.Type.SET);
5178 set.setTo(conversation.getJid().asBareJid());
5179 set.query("http://jabber.org/protocol/muc#owner").addChild(data);
5180 sendIqPacket(
5181 account,
5182 set,
5183 packet -> {
5184 if (callback != null) {
5185 if (packet.getType() == Iq.Type.RESULT) {
5186 callback.onPushSucceeded();
5187 } else {
5188 Log.d(Config.LOGTAG, "failed: " + packet);
5189 callback.onPushFailed();
5190 }
5191 }
5192 });
5193 } else {
5194 if (callback != null) {
5195 callback.onPushFailed();
5196 }
5197 }
5198 });
5199 }
5200
5201 public void pushSubjectToConference(final Conversation conference, final String subject) {
5202 final var packet =
5203 this.getMessageGenerator()
5204 .conferenceSubject(conference, StringUtils.nullOnEmpty(subject));
5205 this.sendMessagePacket(conference.getAccount(), packet);
5206 }
5207
5208 public void requestVoice(final Account account, final Jid jid) {
5209 final var packet = this.getMessageGenerator().requestVoice(jid);
5210 this.sendMessagePacket(account, packet);
5211 }
5212
5213 public void changeAffiliationInConference(
5214 final Conversation conference,
5215 Jid user,
5216 final MucOptions.Affiliation affiliation,
5217 final OnAffiliationChanged callback) {
5218 final Jid jid = user.asBareJid();
5219 final Iq request =
5220 this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
5221 sendIqPacket(
5222 conference.getAccount(),
5223 request,
5224 (response) -> {
5225 if (response.getType() == Iq.Type.RESULT) {
5226 final var mucOptions = conference.getMucOptions();
5227 mucOptions.changeAffiliation(jid, affiliation);
5228 getAvatarService().clear(mucOptions);
5229 if (callback != null) {
5230 callback.onAffiliationChangedSuccessful(jid);
5231 } else {
5232 Log.d(
5233 Config.LOGTAG,
5234 "changed affiliation of " + user + " to " + affiliation);
5235 }
5236 } else if (callback != null) {
5237 callback.onAffiliationChangeFailed(
5238 jid, R.string.could_not_change_affiliation);
5239 } else {
5240 Log.d(Config.LOGTAG, "unable to change affiliation");
5241 }
5242 });
5243 }
5244
5245 public void changeRoleInConference(
5246 final Conversation conference, final String nick, MucOptions.Role role) {
5247 final var account = conference.getAccount();
5248 final Iq request = this.mIqGenerator.changeRole(conference, nick, role.toString());
5249 sendIqPacket(
5250 account,
5251 request,
5252 (packet) -> {
5253 if (packet.getType() != Iq.Type.RESULT) {
5254 Log.d(
5255 Config.LOGTAG,
5256 account.getJid().asBareJid() + " unable to change role of " + nick);
5257 }
5258 });
5259 }
5260
5261 public void moderateMessage(final Account account, final Message m, final String reason) {
5262 final var request = this.mIqGenerator.moderateMessage(account, m, reason);
5263 sendIqPacket(account, request, (packet) -> {
5264 if (packet.getType() != Iq.Type.RESULT) {
5265 showErrorToastInUi(R.string.unable_to_moderate);
5266 Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to moderate: " + packet);
5267 }
5268 });
5269 }
5270
5271 public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) {
5272 final Iq request = new Iq(Iq.Type.SET);
5273 request.setTo(conversation.getJid().asBareJid());
5274 request.query("http://jabber.org/protocol/muc#owner").addChild("destroy");
5275 sendIqPacket(
5276 conversation.getAccount(),
5277 request,
5278 response -> {
5279 if (response.getType() == Iq.Type.RESULT) {
5280 if (callback != null) {
5281 callback.onRoomDestroySucceeded();
5282 }
5283 } else if (response.getType() == Iq.Type.ERROR) {
5284 if (callback != null) {
5285 callback.onRoomDestroyFailed();
5286 }
5287 }
5288 });
5289 }
5290
5291 private void disconnect(final Account account, boolean force) {
5292 final XmppConnection connection = account.getXmppConnection();
5293 if (connection == null) {
5294 return;
5295 }
5296 if (!force) {
5297 final List<Conversation> conversations = getConversations();
5298 for (Conversation conversation : conversations) {
5299 if (conversation.getAccount() == account) {
5300 if (conversation.getMode() == Conversation.MODE_MULTI) {
5301 leaveMuc(conversation, true);
5302 }
5303 }
5304 }
5305 sendOfflinePresence(account);
5306 }
5307 connection.disconnect(force);
5308 }
5309
5310 @Override
5311 public IBinder onBind(Intent intent) {
5312 return mBinder;
5313 }
5314
5315 public void deleteMessage(Message message) {
5316 mScheduledMessages.remove(message.getUuid());
5317 databaseBackend.deleteMessage(message.getUuid());
5318 ((Conversation) message.getConversation()).remove(message);
5319 updateConversationUi();
5320 }
5321
5322 public void updateMessage(Message message) {
5323 updateMessage(message, true);
5324 }
5325
5326 public void updateMessage(Message message, boolean includeBody) {
5327 databaseBackend.updateMessage(message, includeBody);
5328 updateConversationUi();
5329 }
5330
5331 public void createMessageAsync(final Message message) {
5332 mDatabaseWriterExecutor.execute(() -> databaseBackend.createMessage(message));
5333 }
5334
5335 public void updateMessage(Message message, String uuid) {
5336 if (!databaseBackend.updateMessage(message, uuid)) {
5337 Log.e(Config.LOGTAG, "error updated message in DB after edit");
5338 }
5339 updateConversationUi();
5340 }
5341
5342 public void syncDirtyContacts(Account account) {
5343 for (Contact contact : account.getRoster().getContacts()) {
5344 if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
5345 pushContactToServer(contact);
5346 }
5347 if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
5348 deleteContactOnServer(contact);
5349 }
5350 }
5351 }
5352
5353 protected void unregisterPhoneAccounts(final Account account) {
5354 for (final Contact contact : account.getRoster().getContacts()) {
5355 if (!contact.showInRoster()) {
5356 contact.unregisterAsPhoneAccount(this);
5357 }
5358 }
5359 }
5360
5361 public void createContact(final Contact contact, final boolean autoGrant) {
5362 createContact(contact, autoGrant, null);
5363 }
5364
5365 public void createContact(
5366 final Contact contact, final boolean autoGrant, final String preAuth) {
5367 if (autoGrant) {
5368 contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
5369 contact.setOption(Contact.Options.ASKING);
5370 }
5371 pushContactToServer(contact, preAuth);
5372 }
5373
5374 public void pushContactToServer(final Contact contact) {
5375 pushContactToServer(contact, null);
5376 }
5377
5378 private void pushContactToServer(final Contact contact, final String preAuth) {
5379 contact.resetOption(Contact.Options.DIRTY_DELETE);
5380 contact.setOption(Contact.Options.DIRTY_PUSH);
5381 final Account account = contact.getAccount();
5382 if (account.getStatus() == Account.State.ONLINE) {
5383 final boolean ask = contact.getOption(Contact.Options.ASKING);
5384 final boolean sendUpdates =
5385 contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
5386 && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
5387 final Iq iq = new Iq(Iq.Type.SET);
5388 iq.query(Namespace.ROSTER).addChild(contact.asElement());
5389 account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
5390 if (sendUpdates) {
5391 sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
5392 }
5393 if (ask) {
5394 sendPresencePacket(
5395 account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth));
5396 }
5397 } else {
5398 syncRoster(contact.getAccount());
5399 }
5400 }
5401
5402 public void publishMucAvatar(
5403 final Conversation conversation, final Uri image, final OnAvatarPublication callback) {
5404 new Thread(
5405 () -> {
5406 final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
5407 final int size = Config.AVATAR_SIZE;
5408 final Avatar avatar =
5409 getFileBackend().getPepAvatar(image, size, format);
5410 if (avatar != null) {
5411 if (!getFileBackend().save(avatar)) {
5412 callback.onAvatarPublicationFailed(
5413 R.string.error_saving_avatar);
5414 return;
5415 }
5416 avatar.owner = conversation.getJid().asBareJid();
5417 publishMucAvatar(conversation, avatar, callback);
5418 } else {
5419 callback.onAvatarPublicationFailed(
5420 R.string.error_publish_avatar_converting);
5421 }
5422 })
5423 .start();
5424 }
5425
5426 public void publishAvatarAsync(
5427 final Account account,
5428 final Uri image,
5429 final boolean open,
5430 final OnAvatarPublication callback) {
5431 new Thread(() -> publishAvatar(account, image, open, callback)).start();
5432 }
5433
5434 private void publishAvatar(
5435 final Account account,
5436 final Uri image,
5437 final boolean open,
5438 final OnAvatarPublication callback) {
5439 final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
5440 final int size = Config.AVATAR_SIZE;
5441 final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
5442 if (avatar != null) {
5443 if (!getFileBackend().save(avatar)) {
5444 Log.d(Config.LOGTAG, "unable to save vcard");
5445 callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
5446 return;
5447 }
5448 publishAvatar(account, avatar, open, callback);
5449 } else {
5450 callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
5451 }
5452 }
5453
5454 private void publishMucAvatar(
5455 Conversation conversation, Avatar avatar, OnAvatarPublication callback) {
5456 final var account = conversation.getAccount();
5457 final Iq retrieve = mIqGenerator.retrieveVcardAvatar(avatar);
5458 sendIqPacket(
5459 account,
5460 retrieve,
5461 (response) -> {
5462 boolean itemNotFound =
5463 response.getType() == Iq.Type.ERROR
5464 && response.hasChild("error")
5465 && response.findChild("error").hasChild("item-not-found");
5466 if (response.getType() == Iq.Type.RESULT || itemNotFound) {
5467 Element vcard = response.findChild("vCard", "vcard-temp");
5468 if (vcard == null) {
5469 vcard = new Element("vCard", "vcard-temp");
5470 }
5471 Element photo = vcard.findChild("PHOTO");
5472 if (photo == null) {
5473 photo = vcard.addChild("PHOTO");
5474 }
5475 photo.clearChildren();
5476 photo.addChild("TYPE").setContent(avatar.type);
5477 photo.addChild("BINVAL").setContent(avatar.image);
5478 final Iq publication = new Iq(Iq.Type.SET);
5479 publication.setTo(conversation.getJid().asBareJid());
5480 publication.addChild(vcard);
5481 sendIqPacket(
5482 account,
5483 publication,
5484 (publicationResponse) -> {
5485 if (publicationResponse.getType() == Iq.Type.RESULT) {
5486 callback.onAvatarPublicationSucceeded();
5487 } else {
5488 Log.d(
5489 Config.LOGTAG,
5490 "failed to publish vcard "
5491 + publicationResponse.getErrorCondition());
5492 callback.onAvatarPublicationFailed(
5493 R.string.error_publish_avatar_server_reject);
5494 }
5495 });
5496 } else {
5497 Log.d(Config.LOGTAG, "failed to request vcard " + response);
5498 callback.onAvatarPublicationFailed(
5499 R.string.error_publish_avatar_no_server_support);
5500 }
5501 });
5502 }
5503
5504 public void publishAvatar(
5505 final Account account,
5506 final Avatar avatar,
5507 final boolean open,
5508 final OnAvatarPublication callback) {
5509 final Bundle options;
5510 if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
5511 options = open ? PublishOptions.openAccess() : PublishOptions.presenceAccess();
5512 } else {
5513 options = null;
5514 }
5515 publishAvatar(account, avatar, options, true, callback);
5516 }
5517
5518 public void publishAvatar(
5519 Account account,
5520 final Avatar avatar,
5521 final Bundle options,
5522 final boolean retry,
5523 final OnAvatarPublication callback) {
5524 Log.d(
5525 Config.LOGTAG,
5526 account.getJid().asBareJid() + ": publishing avatar. options=" + options);
5527 final Iq packet = this.mIqGenerator.publishAvatar(avatar, options);
5528 this.sendIqPacket(
5529 account,
5530 packet,
5531 result -> {
5532 if (result.getType() == Iq.Type.RESULT) {
5533 publishAvatarMetadata(account, avatar, options, true, callback);
5534 } else if (retry && PublishOptions.preconditionNotMet(result)) {
5535 pushNodeConfiguration(
5536 account,
5537 Namespace.AVATAR_DATA,
5538 options,
5539 new OnConfigurationPushed() {
5540 @Override
5541 public void onPushSucceeded() {
5542 Log.d(
5543 Config.LOGTAG,
5544 account.getJid().asBareJid()
5545 + ": changed node configuration for avatar"
5546 + " node");
5547 publishAvatar(account, avatar, options, false, callback);
5548 }
5549
5550 @Override
5551 public void onPushFailed() {
5552 Log.d(
5553 Config.LOGTAG,
5554 account.getJid().asBareJid()
5555 + ": unable to change node configuration"
5556 + " for avatar node");
5557 publishAvatar(account, avatar, null, false, callback);
5558 }
5559 });
5560 } else {
5561 Element error = result.findChild("error");
5562 Log.d(
5563 Config.LOGTAG,
5564 account.getJid().asBareJid()
5565 + ": server rejected avatar "
5566 + (avatar.size / 1024)
5567 + "KiB "
5568 + (error != null ? error.toString() : ""));
5569 if (callback != null) {
5570 callback.onAvatarPublicationFailed(
5571 R.string.error_publish_avatar_server_reject);
5572 }
5573 }
5574 });
5575 }
5576
5577 public void publishAvatarMetadata(
5578 Account account,
5579 final Avatar avatar,
5580 final Bundle options,
5581 final boolean retry,
5582 final OnAvatarPublication callback) {
5583 final Iq packet =
5584 XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options);
5585 sendIqPacket(
5586 account,
5587 packet,
5588 result -> {
5589 if (result.getType() == Iq.Type.RESULT) {
5590 if (account.setAvatar(avatar.getFilename())) {
5591 getAvatarService().clear(account);
5592 databaseBackend.updateAccount(account);
5593 notifyAccountAvatarHasChanged(account);
5594 }
5595 Log.d(
5596 Config.LOGTAG,
5597 account.getJid().asBareJid()
5598 + ": published avatar "
5599 + (avatar.size / 1024)
5600 + "KiB");
5601 if (callback != null) {
5602 callback.onAvatarPublicationSucceeded();
5603 }
5604 } else if (retry && PublishOptions.preconditionNotMet(result)) {
5605 pushNodeConfiguration(
5606 account,
5607 Namespace.AVATAR_METADATA,
5608 options,
5609 new OnConfigurationPushed() {
5610 @Override
5611 public void onPushSucceeded() {
5612 Log.d(
5613 Config.LOGTAG,
5614 account.getJid().asBareJid()
5615 + ": changed node configuration for avatar"
5616 + " meta data node");
5617 publishAvatarMetadata(
5618 account, avatar, options, false, callback);
5619 }
5620
5621 @Override
5622 public void onPushFailed() {
5623 Log.d(
5624 Config.LOGTAG,
5625 account.getJid().asBareJid()
5626 + ": unable to change node configuration"
5627 + " for avatar meta data node");
5628 publishAvatarMetadata(
5629 account, avatar, null, false, callback);
5630 }
5631 });
5632 } else {
5633 if (callback != null) {
5634 callback.onAvatarPublicationFailed(
5635 R.string.error_publish_avatar_server_reject);
5636 }
5637 }
5638 });
5639 }
5640
5641 public void republishAvatarIfNeeded(final Account account) {
5642 if (account.getAxolotlService().isPepBroken()) {
5643 Log.d(
5644 Config.LOGTAG,
5645 account.getJid().asBareJid()
5646 + ": skipping republication of avatar because pep is broken");
5647 return;
5648 }
5649 final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
5650 this.sendIqPacket(
5651 account,
5652 packet,
5653 new Consumer<Iq>() {
5654
5655 private Avatar parseAvatar(final Iq packet) {
5656 final var pubsub = packet.getExtension(PubSub.class);
5657 if (pubsub == null) {
5658 return null;
5659 }
5660 final var items = pubsub.getItems();
5661 if (items == null) {
5662 return null;
5663 }
5664 final var item = items.getFirstItemWithId(Metadata.class);
5665 if (item == null) {
5666 return null;
5667 }
5668 return Avatar.parseMetadata(item.getKey(), item.getValue());
5669 }
5670
5671 private boolean errorIsItemNotFound(Iq packet) {
5672 Element error = packet.findChild("error");
5673 return packet.getType() == Iq.Type.ERROR
5674 && error != null
5675 && error.hasChild("item-not-found");
5676 }
5677
5678 @Override
5679 public void accept(final Iq packet) {
5680 if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) {
5681 final Avatar serverAvatar = parseAvatar(packet);
5682 if (serverAvatar == null && account.getAvatar() != null) {
5683 final Avatar avatar =
5684 fileBackend.getStoredPepAvatar(account.getAvatar());
5685 if (avatar != null) {
5686 Log.d(
5687 Config.LOGTAG,
5688 account.getJid().asBareJid()
5689 + ": avatar on server was null. republishing");
5690 // publishing as 'open' - old server (that requires
5691 // republication) likely doesn't support access models anyway
5692 publishAvatar(
5693 account,
5694 fileBackend.getStoredPepAvatar(account.getAvatar()),
5695 true,
5696 null);
5697 } else {
5698 Log.e(
5699 Config.LOGTAG,
5700 account.getJid().asBareJid()
5701 + ": error rereading avatar");
5702 }
5703 }
5704 }
5705 }
5706 });
5707 }
5708
5709 public void cancelAvatarFetches(final Account account) {
5710 synchronized (mInProgressAvatarFetches) {
5711 for (final Iterator<String> iterator = mInProgressAvatarFetches.iterator();
5712 iterator.hasNext(); ) {
5713 final String KEY = iterator.next();
5714 if (KEY.startsWith(account.getJid().asBareJid() + "_")) {
5715 iterator.remove();
5716 }
5717 }
5718 }
5719 }
5720
5721 public void fetchAvatar(Account account, Avatar avatar) {
5722 fetchAvatar(account, avatar, null);
5723 }
5724
5725 public void fetchAvatar(
5726 Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5727 if (databaseBackend.isBlockedMedia(avatar.cid())) {
5728 if (callback != null) callback.error(0, null);
5729 return;
5730 }
5731
5732 final String KEY = generateFetchKey(account, avatar);
5733 synchronized (this.mInProgressAvatarFetches) {
5734 if (mInProgressAvatarFetches.add(KEY)) {
5735 switch (avatar.origin) {
5736 case PEP:
5737 this.mInProgressAvatarFetches.add(KEY);
5738 fetchAvatarPep(account, avatar, callback);
5739 break;
5740 case VCARD:
5741 this.mInProgressAvatarFetches.add(KEY);
5742 fetchAvatarVcard(account, avatar, callback);
5743 break;
5744 }
5745 } else if (avatar.origin == Avatar.Origin.PEP) {
5746 mOmittedPepAvatarFetches.add(KEY);
5747 } else {
5748 Log.d(
5749 Config.LOGTAG,
5750 account.getJid().asBareJid()
5751 + ": already fetching "
5752 + avatar.origin
5753 + " avatar for "
5754 + avatar.owner);
5755 }
5756 }
5757 }
5758
5759 private void fetchAvatarPep(
5760 final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5761 final Iq packet = this.mIqGenerator.retrievePepAvatar(avatar);
5762 sendIqPacket(
5763 account,
5764 packet,
5765 (result) -> {
5766 synchronized (mInProgressAvatarFetches) {
5767 mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
5768 }
5769 final String ERROR =
5770 account.getJid().asBareJid()
5771 + ": fetching avatar for "
5772 + avatar.owner
5773 + " failed ";
5774 if (result.getType() == Iq.Type.RESULT) {
5775 avatar.image = IqParser.avatarData(result);
5776 if (avatar.image != null) {
5777 if (getFileBackend().save(avatar)) {
5778 if (account.getJid().asBareJid().equals(avatar.owner)) {
5779 if (account.setAvatar(avatar.getFilename())) {
5780 databaseBackend.updateAccount(account);
5781 }
5782 getAvatarService().clear(account);
5783 updateConversationUi();
5784 updateAccountUi();
5785 } else {
5786 final Contact contact =
5787 account.getRoster().getContact(avatar.owner);
5788 contact.setAvatar(avatar);
5789 syncRoster(account);
5790 getAvatarService().clear(contact);
5791 updateConversationUi();
5792 updateRosterUi(UpdateRosterReason.AVATAR);
5793 }
5794 if (callback != null) {
5795 callback.success(avatar);
5796 }
5797 Log.d(
5798 Config.LOGTAG,
5799 account.getJid().asBareJid()
5800 + ": successfully fetched pep avatar for "
5801 + avatar.owner);
5802 return;
5803 }
5804 } else {
5805
5806 Log.d(Config.LOGTAG, ERROR + "(parsing error)");
5807 }
5808 } else {
5809 Element error = result.findChild("error");
5810 if (error == null) {
5811 Log.d(Config.LOGTAG, ERROR + "(server error)");
5812 } else {
5813 Log.d(Config.LOGTAG, ERROR + error);
5814 }
5815 }
5816 if (callback != null) {
5817 callback.error(0, null);
5818 }
5819 });
5820 }
5821
5822 private void fetchAvatarVcard(
5823 final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5824 final Iq packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
5825 this.sendIqPacket(
5826 account,
5827 packet,
5828 response -> {
5829 final boolean previouslyOmittedPepFetch;
5830 synchronized (mInProgressAvatarFetches) {
5831 final String KEY = generateFetchKey(account, avatar);
5832 mInProgressAvatarFetches.remove(KEY);
5833 previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY);
5834 }
5835 if (response.getType() == Iq.Type.RESULT) {
5836 Element vCard = response.findChild("vCard", "vcard-temp");
5837 Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
5838 String image = photo != null ? photo.findChildContent("BINVAL") : null;
5839 if (image != null) {
5840 avatar.image = image;
5841 if (getFileBackend().save(avatar)) {
5842 Log.d(
5843 Config.LOGTAG,
5844 account.getJid().asBareJid()
5845 + ": successfully fetched vCard avatar for "
5846 + avatar.owner
5847 + " omittedPep="
5848 + previouslyOmittedPepFetch);
5849 if (avatar.owner.isBareJid()) {
5850 if (account.getJid().asBareJid().equals(avatar.owner)
5851 && account.getAvatar() == null) {
5852 Log.d(
5853 Config.LOGTAG,
5854 account.getJid().asBareJid()
5855 + ": had no avatar. replacing with vcard");
5856 account.setAvatar(avatar.getFilename());
5857 databaseBackend.updateAccount(account);
5858 getAvatarService().clear(account);
5859 updateAccountUi();
5860 } else {
5861 final Contact contact =
5862 account.getRoster().getContact(avatar.owner);
5863 contact.setAvatar(avatar, previouslyOmittedPepFetch);
5864 syncRoster(account);
5865 getAvatarService().clear(contact);
5866 updateRosterUi(UpdateRosterReason.AVATAR);
5867 }
5868 updateConversationUi();
5869 } else {
5870 Conversation conversation =
5871 find(account, avatar.owner.asBareJid());
5872 if (conversation != null
5873 && conversation.getMode() == Conversation.MODE_MULTI) {
5874 MucOptions.User user =
5875 conversation
5876 .getMucOptions()
5877 .findUserByFullJid(avatar.owner);
5878 if (user != null) {
5879 if (user.setAvatar(avatar)) {
5880 getAvatarService().clear(user);
5881 updateConversationUi();
5882 updateMucRosterUi();
5883 }
5884 if (user.getRealJid() != null) {
5885 Contact contact =
5886 account.getRoster()
5887 .getContact(user.getRealJid());
5888 contact.setAvatar(avatar);
5889 syncRoster(account);
5890 getAvatarService().clear(contact);
5891 updateRosterUi(UpdateRosterReason.AVATAR);
5892 }
5893 }
5894 }
5895 }
5896 }
5897 }
5898 }
5899 });
5900 }
5901
5902 public void checkForAvatar(final Account account, final UiCallback<Avatar> callback) {
5903 final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
5904 this.sendIqPacket(
5905 account,
5906 packet,
5907 response -> {
5908 if (response.getType() != Iq.Type.RESULT) {
5909 callback.error(0, null);
5910 }
5911 final var pubsub = packet.getExtension(PubSub.class);
5912 if (pubsub == null) {
5913 callback.error(0, null);
5914 return;
5915 }
5916 final var items = pubsub.getItems();
5917 if (items == null) {
5918 callback.error(0, null);
5919 return;
5920 }
5921 final var item = items.getFirstItemWithId(Metadata.class);
5922 if (item == null) {
5923 callback.error(0, null);
5924 return;
5925 }
5926 final var avatar = Avatar.parseMetadata(item.getKey(), item.getValue());
5927 if (avatar == null) {
5928 callback.error(0, null);
5929 return;
5930 }
5931 avatar.owner = account.getJid().asBareJid();
5932 if (fileBackend.isAvatarCached(avatar)) {
5933 if (account.setAvatar(avatar.getFilename())) {
5934 databaseBackend.updateAccount(account);
5935 }
5936 getAvatarService().clear(account);
5937 callback.success(avatar);
5938 } else {
5939 fetchAvatarPep(account, avatar, callback);
5940 }
5941 });
5942 }
5943
5944 public void notifyAccountAvatarHasChanged(final Account account) {
5945 final XmppConnection connection = account.getXmppConnection();
5946 if (connection != null && connection.getFeatures().bookmarksConversion()) {
5947 Log.d(
5948 Config.LOGTAG,
5949 account.getJid().asBareJid()
5950 + ": avatar changed. resending presence to online group chats");
5951 for (Conversation conversation : conversations) {
5952 if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) {
5953 presenceToMuc(conversation);
5954 }
5955 }
5956 }
5957 }
5958
5959 public void fetchVcard4(Account account, final Contact contact, final Consumer<Element> callback) {
5960 final var packet = this.mIqGenerator.retrieveVcard4(contact.getJid());
5961 sendIqPacket(account, packet, (result) -> {
5962 if (result.getType() == Iq.Type.RESULT) {
5963 final Element item = IqParser.getItem(result);
5964 if (item != null) {
5965 final Element vcard4 = item.findChild("vcard", Namespace.VCARD4);
5966 if (vcard4 != null) {
5967 if (callback != null) {
5968 callback.accept(vcard4);
5969 }
5970 return;
5971 }
5972 }
5973 } else {
5974 Element error = result.findChild("error");
5975 if (error == null) {
5976 Log.d(Config.LOGTAG, "fetchVcard4 (server error)");
5977 } else {
5978 Log.d(Config.LOGTAG, "fetchVcard4 " + error.toString());
5979 }
5980 }
5981 if (callback != null) {
5982 callback.accept(null);
5983 }
5984
5985 });
5986 }
5987
5988 public void deleteContactOnServer(Contact contact) {
5989 contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
5990 contact.resetOption(Contact.Options.DIRTY_PUSH);
5991 contact.setOption(Contact.Options.DIRTY_DELETE);
5992 Account account = contact.getAccount();
5993 if (account.getStatus() == Account.State.ONLINE) {
5994 final Iq iq = new Iq(Iq.Type.SET);
5995 Element item = iq.query(Namespace.ROSTER).addChild("item");
5996 item.setAttribute("jid", contact.getJid());
5997 item.setAttribute("subscription", "remove");
5998 account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
5999 }
6000 }
6001
6002 public void updateConversation(final Conversation conversation) {
6003 mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation));
6004 }
6005
6006 private void reconnectAccount(
6007 final Account account, final boolean force, final boolean interactive) {
6008 synchronized (account) {
6009 final XmppConnection existingConnection = account.getXmppConnection();
6010 final XmppConnection connection;
6011 if (existingConnection != null) {
6012 connection = existingConnection;
6013 } else if (account.isConnectionEnabled()) {
6014 connection = createConnection(account);
6015 account.setXmppConnection(connection);
6016 } else {
6017 return;
6018 }
6019 final boolean hasInternet = hasInternetConnection();
6020 if (account.isConnectionEnabled() && hasInternet) {
6021 if (!force) {
6022 disconnect(account, false);
6023 }
6024 Thread thread = new Thread(connection);
6025 connection.setInteractive(interactive);
6026 connection.prepareNewConnection();
6027 connection.interrupt();
6028 thread.start();
6029 scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
6030 } else {
6031 disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
6032 account.getRoster().clearPresences();
6033 connection.resetEverything();
6034 final AxolotlService axolotlService = account.getAxolotlService();
6035 if (axolotlService != null) {
6036 axolotlService.resetBrokenness();
6037 }
6038 if (!hasInternet) {
6039 account.setStatus(Account.State.NO_INTERNET);
6040 }
6041 }
6042 }
6043 }
6044
6045 public void reconnectAccountInBackground(final Account account) {
6046 new Thread(() -> reconnectAccount(account, false, true)).start();
6047 }
6048
6049 public void invite(final Conversation conversation, final Jid contact) {
6050 Log.d(
6051 Config.LOGTAG,
6052 conversation.getAccount().getJid().asBareJid()
6053 + ": inviting "
6054 + contact
6055 + " to "
6056 + conversation.getJid().asBareJid());
6057 final MucOptions.User user =
6058 conversation.getMucOptions().findUserByRealJid(contact.asBareJid());
6059 if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) {
6060 changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null);
6061 }
6062 final var packet = mMessageGenerator.invite(conversation, contact);
6063 sendMessagePacket(conversation.getAccount(), packet);
6064 }
6065
6066 public void directInvite(Conversation conversation, Jid jid) {
6067 final var packet = mMessageGenerator.directInvite(conversation, jid);
6068 sendMessagePacket(conversation.getAccount(), packet);
6069 }
6070
6071 public void resetSendingToWaiting(Account account) {
6072 for (Conversation conversation : getConversations()) {
6073 if (conversation.getAccount() == account) {
6074 conversation.findUnsentTextMessages(
6075 message -> markMessage(message, Message.STATUS_WAITING));
6076 }
6077 }
6078 }
6079
6080 public Message markMessage(
6081 final Account account, final Jid recipient, final String uuid, final int status) {
6082 return markMessage(account, recipient, uuid, status, null);
6083 }
6084
6085 public Message markMessage(
6086 final Account account,
6087 final Jid recipient,
6088 final String uuid,
6089 final int status,
6090 String errorMessage) {
6091 if (uuid == null) {
6092 return null;
6093 }
6094 for (Conversation conversation : getConversations()) {
6095 if (conversation.getJid().asBareJid().equals(recipient)
6096 && conversation.getAccount() == account) {
6097 final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
6098 if (message != null) {
6099 markMessage(message, status, errorMessage);
6100 }
6101 return message;
6102 }
6103 }
6104 return null;
6105 }
6106
6107 public boolean markMessage(
6108 final Conversation conversation,
6109 final String uuid,
6110 final int status,
6111 final String serverMessageId) {
6112 return markMessage(conversation, uuid, status, serverMessageId, null, null, null, null, null);
6113 }
6114
6115 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) {
6116 if (uuid == null) {
6117 return false;
6118 } else {
6119 final Message message = conversation.findSentMessageWithUuid(uuid);
6120 if (message != null) {
6121 if (message.getServerMsgId() == null) {
6122 message.setServerMsgId(serverMessageId);
6123 }
6124 if (message.getEncryption() == Message.ENCRYPTION_NONE && (body != null || html != null || subject != null || thread != null || attachments != null)) {
6125 message.setBody(body.content);
6126 if (body.count > 1) {
6127 message.setBodyLanguage(body.language);
6128 }
6129 message.setHtml(html);
6130 message.setSubject(subject);
6131 message.setThread(thread);
6132 if (attachments != null && attachments.isEmpty()) {
6133 message.setRelativeFilePath(null);
6134 message.resetFileParams();
6135 }
6136 markMessage(message, status, null, true);
6137 } else {
6138 markMessage(message, status);
6139 }
6140 return true;
6141 } else {
6142 return false;
6143 }
6144 }
6145 }
6146
6147 public void markMessage(Message message, int status) {
6148 markMessage(message, status, null);
6149 }
6150
6151 public void markMessage(final Message message, final int status, final String errorMessage) {
6152 markMessage(message, status, errorMessage, false);
6153 }
6154
6155 public void markMessage(
6156 final Message message,
6157 final int status,
6158 final String errorMessage,
6159 final boolean includeBody) {
6160 final int oldStatus = message.getStatus();
6161 if (status == Message.STATUS_SEND_FAILED
6162 && (oldStatus == Message.STATUS_SEND_RECEIVED
6163 || oldStatus == Message.STATUS_SEND_DISPLAYED)) {
6164 return;
6165 }
6166 if (status == Message.STATUS_SEND_RECEIVED && oldStatus == Message.STATUS_SEND_DISPLAYED) {
6167 return;
6168 }
6169 message.setErrorMessage(errorMessage);
6170 message.setStatus(status);
6171 databaseBackend.updateMessage(message, includeBody);
6172 updateConversationUi();
6173 if (oldStatus != status && status == Message.STATUS_SEND_FAILED) {
6174 mNotificationService.pushFailedDelivery(message);
6175 }
6176 }
6177
6178 public SharedPreferences getPreferences() {
6179 return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
6180 }
6181
6182 public long getAutomaticMessageDeletionDate() {
6183 final long timeout =
6184 getLongPreference(
6185 AppSettings.AUTOMATIC_MESSAGE_DELETION,
6186 R.integer.automatic_message_deletion);
6187 return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000));
6188 }
6189
6190 public long getLongPreference(String name, @IntegerRes int res) {
6191 long defaultValue = getResources().getInteger(res);
6192 try {
6193 return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue)));
6194 } catch (NumberFormatException e) {
6195 return defaultValue;
6196 }
6197 }
6198
6199 public boolean getBooleanPreference(String name, @BoolRes int res) {
6200 return getPreferences().getBoolean(name, getResources().getBoolean(res));
6201 }
6202
6203 public String getStringPreference(String name, @BoolRes int res) {
6204 return getPreferences().getString(name, getResources().getString(res));
6205 }
6206
6207 public boolean confirmMessages() {
6208 return appSettings.isConfirmMessages();
6209 }
6210
6211 public boolean allowMessageCorrection() {
6212 return appSettings.isAllowMessageCorrection();
6213 }
6214
6215 public boolean sendChatStates() {
6216 return getBooleanPreference("chat_states", R.bool.chat_states);
6217 }
6218
6219 public boolean useTorToConnect() {
6220 return appSettings.isUseTor();
6221 }
6222
6223 public boolean broadcastLastActivity() {
6224 return appSettings.isBroadcastLastActivity();
6225 }
6226
6227 public int unreadCount() {
6228 int count = 0;
6229 for (Conversation conversation : getConversations()) {
6230 count += conversation.unreadCount(this);
6231 }
6232 return count;
6233 }
6234
6235 private <T> List<T> threadSafeList(Set<T> set) {
6236 synchronized (LISTENER_LOCK) {
6237 return set.isEmpty() ? Collections.emptyList() : new ArrayList<>(set);
6238 }
6239 }
6240
6241 public void showErrorToastInUi(int resId) {
6242 for (OnShowErrorToast listener : threadSafeList(this.mOnShowErrorToasts)) {
6243 listener.onShowErrorToast(resId);
6244 }
6245 }
6246
6247 public void updateConversationUi() {
6248 updateConversationUi(false);
6249 }
6250
6251 public void updateConversationUi(boolean newCaps) {
6252 for (OnConversationUpdate listener : threadSafeList(this.mOnConversationUpdates)) {
6253 listener.onConversationUpdate(newCaps);
6254 }
6255 }
6256
6257 public void notifyJingleRtpConnectionUpdate(
6258 final Account account,
6259 final Jid with,
6260 final String sessionId,
6261 final RtpEndUserState state) {
6262 for (OnJingleRtpConnectionUpdate listener :
6263 threadSafeList(this.onJingleRtpConnectionUpdate)) {
6264 listener.onJingleRtpConnectionUpdate(account, with, sessionId, state);
6265 }
6266 }
6267
6268 public void notifyJingleRtpConnectionUpdate(
6269 CallIntegration.AudioDevice selectedAudioDevice,
6270 Set<CallIntegration.AudioDevice> availableAudioDevices) {
6271 for (OnJingleRtpConnectionUpdate listener :
6272 threadSafeList(this.onJingleRtpConnectionUpdate)) {
6273 listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
6274 }
6275 }
6276
6277 public void updateAccountUi() {
6278 for (final OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) {
6279 listener.onAccountUpdate();
6280 }
6281 }
6282
6283 public void updateRosterUi(final UpdateRosterReason reason) {
6284 if (reason == UpdateRosterReason.PRESENCE) throw new IllegalArgumentException("PRESENCE must also come with a contact");
6285 updateRosterUi(reason, null);
6286 }
6287
6288 public void updateRosterUi(final UpdateRosterReason reason, final Contact contact) {
6289 for (OnRosterUpdate listener : threadSafeList(this.mOnRosterUpdates)) {
6290 listener.onRosterUpdate(reason, contact);
6291 }
6292 }
6293
6294 public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
6295 if (mOnCaptchaRequested.size() > 0) {
6296 DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
6297 Bitmap scaled =
6298 Bitmap.createScaledBitmap(
6299 captcha,
6300 (int) (captcha.getWidth() * metrics.scaledDensity),
6301 (int) (captcha.getHeight() * metrics.scaledDensity),
6302 false);
6303 for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
6304 listener.onCaptchaRequested(account, id, data, scaled);
6305 }
6306 return true;
6307 }
6308 return false;
6309 }
6310
6311 public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
6312 for (OnUpdateBlocklist listener : threadSafeList(this.mOnUpdateBlocklist)) {
6313 listener.OnUpdateBlocklist(status);
6314 }
6315 }
6316
6317 public void updateMucRosterUi() {
6318 for (OnMucRosterUpdate listener : threadSafeList(this.mOnMucRosterUpdate)) {
6319 listener.onMucRosterUpdate();
6320 }
6321 }
6322
6323 public void keyStatusUpdated(AxolotlService.FetchStatus report) {
6324 for (OnKeyStatusUpdated listener : threadSafeList(this.mOnKeyStatusUpdated)) {
6325 listener.onKeyStatusUpdated(report);
6326 }
6327 }
6328
6329 public Account findAccountByJid(final Jid jid) {
6330 for (final Account account : this.accounts) {
6331 if (account.getJid().asBareJid().equals(jid.asBareJid())) {
6332 return account;
6333 }
6334 }
6335 return null;
6336 }
6337
6338 public Account findAccountByUuid(final String uuid) {
6339 for (Account account : this.accounts) {
6340 if (account.getUuid().equals(uuid)) {
6341 return account;
6342 }
6343 }
6344 return null;
6345 }
6346
6347 public Conversation findConversationByUuid(String uuid) {
6348 for (Conversation conversation : getConversations()) {
6349 if (conversation.getUuid().equals(uuid)) {
6350 return conversation;
6351 }
6352 }
6353 return null;
6354 }
6355
6356 public Conversation findUniqueConversationByJid(XmppUri xmppUri) {
6357 List<Conversation> findings = new ArrayList<>();
6358 for (Conversation c : getConversations()) {
6359 if (c.getAccount().isEnabled()
6360 && c.getJid().asBareJid().equals(xmppUri.getJid().asBareJid())
6361 && ((c.getMode() == Conversational.MODE_MULTI)
6362 == xmppUri.isAction(XmppUri.ACTION_JOIN))) {
6363 findings.add(c);
6364 }
6365 }
6366 return findings.size() == 1 ? findings.get(0) : null;
6367 }
6368
6369 public boolean markRead(final Conversation conversation, boolean dismiss) {
6370 return markRead(conversation, null, dismiss).size() > 0;
6371 }
6372
6373 public void markRead(final Conversation conversation) {
6374 markRead(conversation, null, true);
6375 }
6376
6377 public List<Message> markRead(
6378 final Conversation conversation, String upToUuid, boolean dismiss) {
6379 if (dismiss) {
6380 mNotificationService.clear(conversation);
6381 }
6382 final List<Message> readMessages = conversation.markRead(upToUuid);
6383 if (readMessages.size() > 0) {
6384 Runnable runnable =
6385 () -> {
6386 for (Message message : readMessages) {
6387 databaseBackend.updateMessage(message, false);
6388 }
6389 };
6390 mDatabaseWriterExecutor.execute(runnable);
6391 updateConversationUi();
6392 updateUnreadCountBadge();
6393 return readMessages;
6394 } else {
6395 return readMessages;
6396 }
6397 }
6398
6399 public void markNotificationDismissed(final List<Message> messages) {
6400 Runnable runnable = () -> {
6401 for (final var message : messages) {
6402 message.markNotificationDismissed();
6403 databaseBackend.updateMessage(message, false);
6404 }
6405 };
6406 mDatabaseWriterExecutor.execute(runnable);
6407 }
6408
6409 public synchronized void updateUnreadCountBadge() {
6410 int count = unreadCount();
6411 if (unreadCount != count) {
6412 Log.d(Config.LOGTAG, "update unread count to " + count);
6413 if (count > 0) {
6414 ShortcutBadger.applyCount(getApplicationContext(), count);
6415 } else {
6416 ShortcutBadger.removeCount(getApplicationContext());
6417 }
6418 unreadCount = count;
6419 }
6420 }
6421
6422 public void sendReadMarker(final Conversation conversation, final String upToUuid) {
6423 final boolean isPrivateAndNonAnonymousMuc =
6424 conversation.getMode() == Conversation.MODE_MULTI
6425 && conversation.isPrivateAndNonAnonymous();
6426 final List<Message> readMessages = this.markRead(conversation, upToUuid, true);
6427 if (readMessages.isEmpty()) {
6428 return;
6429 }
6430 final var account = conversation.getAccount();
6431 final var connection = account.getXmppConnection();
6432 updateConversationUi();
6433 final var last =
6434 Iterables.getLast(
6435 Collections2.filter(
6436 readMessages,
6437 m ->
6438 !m.isPrivateMessage()
6439 && m.getStatus() == Message.STATUS_RECEIVED),
6440 null);
6441 if (last == null) {
6442 return;
6443 }
6444
6445 final boolean sendDisplayedMarker =
6446 confirmMessages()
6447 && (last.trusted() || isPrivateAndNonAnonymousMuc)
6448 && last.getRemoteMsgId() != null
6449 && (last.markable || isPrivateAndNonAnonymousMuc);
6450 final boolean serverAssist =
6451 connection != null && connection.getFeatures().mdsServerAssist();
6452
6453 final String stanzaId = last.getServerMsgId();
6454
6455 if (sendDisplayedMarker && serverAssist) {
6456 final var mdsDisplayed = mIqGenerator.mdsDisplayed(stanzaId, conversation);
6457 final var packet = mMessageGenerator.confirm(last);
6458 packet.addChild(mdsDisplayed);
6459 if (!last.isPrivateMessage()) {
6460 packet.setTo(packet.getTo().asBareJid());
6461 }
6462 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server assisted " + packet);
6463 this.sendMessagePacket(account, packet);
6464 } else {
6465 publishMds(last);
6466 // read markers will be sent after MDS to flush the CSI stanza queue
6467 if (sendDisplayedMarker) {
6468 Log.d(
6469 Config.LOGTAG,
6470 conversation.getAccount().getJid().asBareJid()
6471 + ": sending displayed marker to "
6472 + last.getCounterpart().toString());
6473 final var packet = mMessageGenerator.confirm(last);
6474 this.sendMessagePacket(account, packet);
6475 }
6476 }
6477 }
6478
6479 private void publishMds(@Nullable final Message message) {
6480 final String stanzaId = message == null ? null : message.getServerMsgId();
6481 if (Strings.isNullOrEmpty(stanzaId)) {
6482 return;
6483 }
6484 final Conversation conversation;
6485 final var conversational = message.getConversation();
6486 if (conversational instanceof Conversation c) {
6487 conversation = c;
6488 } else {
6489 return;
6490 }
6491 final var account = conversation.getAccount();
6492 final var connection = account.getXmppConnection();
6493 if (connection == null || !connection.getFeatures().mds()) {
6494 return;
6495 }
6496 final Jid itemId;
6497 if (message.isPrivateMessage()) {
6498 itemId = message.getCounterpart();
6499 } else {
6500 itemId = conversation.getJid().asBareJid();
6501 }
6502 Log.d(Config.LOGTAG, "publishing mds for " + itemId + "/" + stanzaId);
6503 publishMds(account, itemId, stanzaId, conversation);
6504 }
6505
6506 private void publishMds(
6507 final Account account,
6508 final Jid itemId,
6509 final String stanzaId,
6510 final Conversation conversation) {
6511 final var item = mIqGenerator.mdsDisplayed(stanzaId, conversation);
6512 pushNodeAndEnforcePublishOptions(
6513 account,
6514 Namespace.MDS_DISPLAYED,
6515 item,
6516 itemId.toString(),
6517 PublishOptions.persistentWhitelistAccessMaxItems());
6518 }
6519
6520 public boolean sendReactions(final Message message, final Collection<String> reactions) {
6521 if (message.isPrivateMessage()) throw new IllegalArgumentException("Reactions to PM not implemented");
6522 if (message.getConversation() instanceof Conversation conversation) {
6523 final var isPrivateMessage = message.isPrivateMessage();
6524 final Jid reactTo;
6525 final boolean typeGroupChat;
6526 final String reactToId;
6527 final Collection<Reaction> combinedReactions;
6528 final var newReactions = new HashSet<>(reactions);
6529 newReactions.removeAll(message.getAggregatedReactions().ourReactions);
6530 if (conversation.getMode() == Conversational.MODE_MULTI && !isPrivateMessage) {
6531 final var mucOptions = conversation.getMucOptions();
6532 if (!mucOptions.participating()) {
6533 Log.e(Config.LOGTAG, "not participating in MUC");
6534 return false;
6535 }
6536 final var self = mucOptions.getSelf();
6537 final String occupantId = self.getOccupantId();
6538 if (Strings.isNullOrEmpty(occupantId)) {
6539 Log.e(Config.LOGTAG, "occupant id not found for reaction in MUC");
6540 return false;
6541 }
6542 final var existingRaw =
6543 ImmutableSet.copyOf(
6544 Collections2.transform(message.getReactions(), r -> r.reaction));
6545 final var reactionsAsExistingVariants =
6546 ImmutableSet.copyOf(
6547 Collections2.transform(
6548 reactions, r -> Emoticons.existingVariant(r, existingRaw)));
6549 if (!reactions.equals(reactionsAsExistingVariants)) {
6550 Log.d(Config.LOGTAG, "modified reactions to existing variants");
6551 }
6552 reactToId = message.getServerMsgId();
6553 reactTo = conversation.getJid().asBareJid();
6554 typeGroupChat = true;
6555 combinedReactions =
6556 Reaction.withMine(
6557 message.getReactions(),
6558 reactionsAsExistingVariants,
6559 false,
6560 self.getFullJid(),
6561 conversation.getAccount().getJid(),
6562 occupantId,
6563 null);
6564 } else {
6565 if (message.isCarbon() || message.getStatus() == Message.STATUS_RECEIVED) {
6566 reactToId = message.getRemoteMsgId();
6567 } else {
6568 reactToId = message.getUuid();
6569 }
6570 typeGroupChat = false;
6571 if (isPrivateMessage) {
6572 reactTo = message.getCounterpart();
6573 } else {
6574 reactTo = conversation.getJid().asBareJid();
6575 }
6576 combinedReactions =
6577 Reaction.withFrom(
6578 message.getReactions(),
6579 reactions,
6580 false,
6581 conversation.getAccount().getJid(),
6582 null);
6583 }
6584 if (reactTo == null || Strings.isNullOrEmpty(reactToId)) {
6585 Log.e(Config.LOGTAG, "could not find id to react to");
6586 return false;
6587 }
6588
6589 final var packet =
6590 mMessageGenerator.reaction(reactTo, typeGroupChat, message, reactToId, reactions);
6591
6592 final var quote = QuoteHelper.quote(MessageUtils.prepareQuote(message)) + "\n";
6593 final var body = quote + String.join(" ", newReactions);
6594 if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && newReactions.size() > 0) {
6595 FILE_ATTACHMENT_EXECUTOR.execute(() -> {
6596 XmppAxolotlMessage axolotlMessage = conversation.getAccount().getAxolotlService().encrypt(body, conversation);
6597 packet.setAxolotlMessage(axolotlMessage.toElement());
6598 packet.addChild("encryption", "urn:xmpp:eme:0")
6599 .setAttribute("name", "OMEMO")
6600 .setAttribute("namespace", AxolotlService.PEP_PREFIX);
6601 sendMessagePacket(conversation.getAccount(), packet);
6602 message.setReactions(combinedReactions);
6603 updateMessage(message, false);
6604 });
6605 } else if (conversation.getNextEncryption() == Message.ENCRYPTION_NONE || newReactions.size() < 1) {
6606 if (newReactions.size() > 0) {
6607 packet.setBody(body);
6608
6609 packet.addChild("reply", "urn:xmpp:reply:0")
6610 .setAttribute("to", message.getCounterpart())
6611 .setAttribute("id", reactToId);
6612 final var replyFallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reply:0");
6613 replyFallback.addChild("body", "urn:xmpp:fallback:0")
6614 .setAttribute("start", "0")
6615 .setAttribute("end", "" + quote.codePointCount(0, quote.length()));
6616
6617 final var fallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reactions:0");
6618 fallback.addChild("body", "urn:xmpp:fallback:0");
6619 }
6620
6621 sendMessagePacket(conversation.getAccount(), packet);
6622 message.setReactions(combinedReactions);
6623 updateMessage(message, false);
6624 }
6625
6626 return true;
6627 } else {
6628 return false;
6629 }
6630 }
6631
6632 public MemorizingTrustManager getMemorizingTrustManager() {
6633 return this.mMemorizingTrustManager;
6634 }
6635
6636 public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
6637 this.mMemorizingTrustManager = trustManager;
6638 }
6639
6640 public void updateMemorizingTrustManager() {
6641 final MemorizingTrustManager trustManager;
6642 if (appSettings.isTrustSystemCAStore()) {
6643 trustManager = new MemorizingTrustManager(getApplicationContext());
6644 } else {
6645 trustManager = new MemorizingTrustManager(getApplicationContext(), null);
6646 }
6647 setMemorizingTrustManager(trustManager);
6648 }
6649
6650 public LruCache<String, Drawable> getDrawableCache() {
6651 return this.mDrawableCache;
6652 }
6653
6654 public Collection<String> getKnownHosts() {
6655 final Set<String> hosts = new HashSet<>();
6656 for (final Account account : getAccounts()) {
6657 hosts.add(account.getServer());
6658 for (final Contact contact : account.getRoster().getContacts()) {
6659 if (contact.showInRoster()) {
6660 final String server = contact.getServer();
6661 if (server != null) {
6662 hosts.add(server);
6663 }
6664 }
6665 }
6666 }
6667 if (Config.QUICKSY_DOMAIN != null) {
6668 hosts.remove(
6669 Config.QUICKSY_DOMAIN
6670 .toString()); // we only want to show this when we type a e164
6671 // number
6672 }
6673 if (Config.MAGIC_CREATE_DOMAIN != null) {
6674 hosts.add(Config.MAGIC_CREATE_DOMAIN);
6675 }
6676 hosts.add("chat.above.im");
6677 return hosts;
6678 }
6679
6680 public Collection<String> getKnownConferenceHosts() {
6681 final Set<String> mucServers = new HashSet<>();
6682 for (final Account account : accounts) {
6683 if (account.getXmppConnection() != null) {
6684 mucServers.addAll(account.getXmppConnection().getMucServers());
6685 for (final Bookmark bookmark : account.getBookmarks()) {
6686 final Jid jid = bookmark.getJid();
6687 final String s = jid == null ? null : jid.getDomain().toString();
6688 if (s != null) {
6689 mucServers.add(s);
6690 }
6691 }
6692 }
6693 }
6694 return mucServers;
6695 }
6696
6697 public void sendMessagePacket(
6698 final Account account,
6699 final im.conversations.android.xmpp.model.stanza.Message packet) {
6700 final XmppConnection connection = account.getXmppConnection();
6701 if (connection != null) {
6702 connection.sendMessagePacket(packet);
6703 }
6704 }
6705
6706 public void sendPresencePacket(
6707 final Account account,
6708 final im.conversations.android.xmpp.model.stanza.Presence packet) {
6709 final XmppConnection connection = account.getXmppConnection();
6710 if (connection != null) {
6711 connection.sendPresencePacket(packet);
6712 }
6713 }
6714
6715 public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
6716 final XmppConnection connection = account.getXmppConnection();
6717 if (connection == null) {
6718 return;
6719 }
6720 connection.sendCreateAccountWithCaptchaPacket(id, data);
6721 }
6722
6723 public ListenableFuture<Iq> sendIqPacket(final Account account, final Iq request) {
6724 final XmppConnection connection = account.getXmppConnection();
6725 if (connection == null) {
6726 return Futures.immediateFailedFuture(new TimeoutException());
6727 }
6728 return connection.sendIqPacket(request);
6729 }
6730
6731 public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback) {
6732 sendIqPacket(account, packet, callback, null);
6733 }
6734
6735 public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback, Long timeout) {
6736 final XmppConnection connection = account.getXmppConnection();
6737 if (connection != null) {
6738 connection.sendIqPacket(packet, callback, timeout);
6739 } else if (callback != null) {
6740 callback.accept(Iq.TIMEOUT);
6741 }
6742 }
6743
6744 public void sendPresence(final Account account) {
6745 sendPresence(account, checkListeners() && broadcastLastActivity());
6746 }
6747
6748 private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
6749 final im.conversations.android.xmpp.model.stanza.Presence.Availability status;
6750 if (manuallyChangePresence()) {
6751 status = account.getPresenceStatus();
6752 } else {
6753 status = getTargetPresence();
6754 }
6755 final var packet = mPresenceGenerator.selfPresence(account, status);
6756 if (mLastActivity > 0 && includeIdleTimestamp) {
6757 long since =
6758 Math.min(mLastActivity, System.currentTimeMillis()); // don't send future dates
6759 packet.addChild("idle", Namespace.IDLE)
6760 .setAttribute("since", AbstractGenerator.getTimestamp(since));
6761 }
6762 sendPresencePacket(account, packet);
6763 }
6764
6765 private void deactivateGracePeriod() {
6766 for (Account account : getAccounts()) {
6767 account.deactivateGracePeriod();
6768 }
6769 }
6770
6771 public void refreshAllPresences() {
6772 boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
6773 for (Account account : getAccounts()) {
6774 if (account.isConnectionEnabled()) {
6775 sendPresence(account, includeIdleTimestamp);
6776 }
6777 }
6778 }
6779
6780 private void refreshAllFcmTokens() {
6781 for (Account account : getAccounts()) {
6782 if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
6783 mPushManagementService.registerPushTokenOnServer(account);
6784 }
6785 }
6786 }
6787
6788 private void sendOfflinePresence(final Account account) {
6789 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence");
6790 sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
6791 }
6792
6793 public MessageGenerator getMessageGenerator() {
6794 return this.mMessageGenerator;
6795 }
6796
6797 public PresenceGenerator getPresenceGenerator() {
6798 return this.mPresenceGenerator;
6799 }
6800
6801 public IqGenerator getIqGenerator() {
6802 return this.mIqGenerator;
6803 }
6804
6805 public JingleConnectionManager getJingleConnectionManager() {
6806 return this.mJingleConnectionManager;
6807 }
6808
6809 private boolean hasJingleRtpConnection(final Account account) {
6810 return this.mJingleConnectionManager.hasJingleRtpConnection(account);
6811 }
6812
6813 public MessageArchiveService getMessageArchiveService() {
6814 return this.mMessageArchiveService;
6815 }
6816
6817 public QuickConversationsService getQuickConversationsService() {
6818 return this.mQuickConversationsService;
6819 }
6820
6821 public List<Contact> findContacts(Jid jid, String accountJid) {
6822 ArrayList<Contact> contacts = new ArrayList<>();
6823 for (Account account : getAccounts()) {
6824 if ((account.isEnabled() || accountJid != null)
6825 && (accountJid == null
6826 || accountJid.equals(account.getJid().asBareJid().toString()))) {
6827 Contact contact = account.getRoster().getContactFromContactList(jid);
6828 if (contact != null) {
6829 contacts.add(contact);
6830 }
6831 }
6832 }
6833 return contacts;
6834 }
6835
6836 public Conversation findFirstMuc(Jid jid) {
6837 return findFirstMuc(jid, null);
6838 }
6839
6840 public Conversation findFirstMuc(Jid jid, String accountJid) {
6841 for (Conversation conversation : getConversations()) {
6842 if ((conversation.getAccount().isEnabled() || accountJid != null)
6843 && (accountJid == null || accountJid.equals(conversation.getAccount().getJid().asBareJid().toString()))
6844 && conversation.getJid().asBareJid().equals(jid.asBareJid()) && conversation.getMode() == Conversation.MODE_MULTI) {
6845 return conversation;
6846 }
6847 }
6848 return null;
6849 }
6850
6851 public NotificationService getNotificationService() {
6852 return this.mNotificationService;
6853 }
6854
6855 public HttpConnectionManager getHttpConnectionManager() {
6856 return this.mHttpConnectionManager;
6857 }
6858
6859 public void resendFailedMessages(final Message message, final boolean forceP2P) {
6860 message.setTime(System.currentTimeMillis());
6861 markMessage(message, Message.STATUS_WAITING);
6862 this.sendMessage(message, true, false, false, forceP2P, null);
6863 if (message.getConversation() instanceof Conversation c) {
6864 c.sort();
6865 }
6866 updateConversationUi();
6867 }
6868
6869 public void clearConversationHistory(final Conversation conversation) {
6870 final long clearDate;
6871 final String reference;
6872 if (conversation.countMessages() > 0) {
6873 Message latestMessage = conversation.getLatestMessage();
6874 clearDate = latestMessage.getTimeSent() + 1000;
6875 reference = latestMessage.getServerMsgId();
6876 } else {
6877 clearDate = System.currentTimeMillis();
6878 reference = null;
6879 }
6880 conversation.clearMessages();
6881 conversation.setHasMessagesLeftOnServer(false); // avoid messages getting loaded through mam
6882 conversation.setLastClearHistory(clearDate, reference);
6883 Runnable runnable =
6884 () -> {
6885 databaseBackend.deleteMessagesInConversation(conversation);
6886 databaseBackend.updateConversation(conversation);
6887 };
6888 mDatabaseWriterExecutor.execute(runnable);
6889 }
6890
6891 public boolean sendBlockRequest(
6892 final Blockable blockable, final boolean reportSpam, final String serverMsgId) {
6893 if (blockable != null && blockable.getBlockedJid() != null) {
6894 final var account = blockable.getAccount();
6895 final Jid jid = blockable.getBlockedJid();
6896 this.sendIqPacket(
6897 account,
6898 getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId),
6899 (response) -> {
6900 if (response.getType() == Iq.Type.RESULT) {
6901 account.getBlocklist().add(jid);
6902 updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
6903 }
6904 });
6905 if (blockable.getBlockedJid().isFullJid()) {
6906 return false;
6907 } else if (removeBlockedConversations(blockable.getAccount(), jid)) {
6908 updateConversationUi();
6909 return true;
6910 } else {
6911 return false;
6912 }
6913 } else {
6914 return false;
6915 }
6916 }
6917
6918 public boolean removeBlockedConversations(final Account account, final Jid blockedJid) {
6919 boolean removed = false;
6920 synchronized (this.conversations) {
6921 boolean domainJid = blockedJid.getLocal() == null;
6922 for (Conversation conversation : this.conversations) {
6923 boolean jidMatches =
6924 (domainJid
6925 && blockedJid
6926 .getDomain()
6927 .equals(conversation.getJid().getDomain()))
6928 || blockedJid.equals(conversation.getJid().asBareJid());
6929 if (conversation.getAccount() == account
6930 && conversation.getMode() == Conversation.MODE_SINGLE
6931 && jidMatches) {
6932 this.conversations.remove(conversation);
6933 markRead(conversation);
6934 conversation.setStatus(Conversation.STATUS_ARCHIVED);
6935 Log.d(
6936 Config.LOGTAG,
6937 account.getJid().asBareJid()
6938 + ": archiving conversation "
6939 + conversation.getJid().asBareJid()
6940 + " because jid was blocked");
6941 updateConversation(conversation);
6942 removed = true;
6943 }
6944 }
6945 }
6946 return removed;
6947 }
6948
6949 public void sendUnblockRequest(final Blockable blockable) {
6950 if (blockable != null && blockable.getJid() != null) {
6951 final var account = blockable.getAccount();
6952 final Jid jid = blockable.getBlockedJid();
6953 this.sendIqPacket(
6954 account,
6955 getIqGenerator().generateSetUnblockRequest(jid),
6956 response -> {
6957 if (response.getType() == Iq.Type.RESULT) {
6958 account.getBlocklist().remove(jid);
6959 updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
6960 }
6961 });
6962 }
6963 }
6964
6965 public void publishDisplayName(final Account account) {
6966 String displayName = account.getDisplayName();
6967 final Iq request;
6968 if (TextUtils.isEmpty(displayName)) {
6969 request = mIqGenerator.deleteNode(Namespace.NICK);
6970 } else {
6971 request = mIqGenerator.publishNick(displayName);
6972 }
6973 mAvatarService.clear(account);
6974 sendIqPacket(
6975 account,
6976 request,
6977 (packet) -> {
6978 if (packet.getType() == Iq.Type.ERROR) {
6979 Log.d(
6980 Config.LOGTAG,
6981 account.getJid().asBareJid()
6982 + ": unable to modify nick name "
6983 + packet);
6984 }
6985 });
6986 }
6987
6988 public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) {
6989 final var request = new Iq(input == null ? Iq.Type.GET : Iq.Type.SET);
6990 request.setTo(jid);
6991 Element query = request.query("jabber:iq:gateway");
6992 if (input != null) {
6993 Element prompt = query.addChild("prompt");
6994 prompt.setContent(input);
6995 }
6996 sendIqPacket(account, request, packet -> {
6997 if (packet.getType() == Iq.Type.RESULT) {
6998 callback.onGatewayResult(packet.query().findChildContent(input == null ? "prompt" : "jid"), null);
6999 } else {
7000 Element error = packet.findChild("error");
7001 callback.onGatewayResult(null, error == null ? null : error.findChildContent("text"));
7002 }
7003 });
7004 }
7005
7006 public void fetchMamPreferences(final Account account, final OnMamPreferencesFetched callback) {
7007 final MessageArchiveService.Version version = MessageArchiveService.Version.get(account);
7008 final Iq request = new Iq(Iq.Type.GET);
7009 request.addChild("prefs", version.namespace);
7010 sendIqPacket(
7011 account,
7012 request,
7013 (packet) -> {
7014 final Element prefs = packet.findChild("prefs", version.namespace);
7015 if (packet.getType() == Iq.Type.RESULT && prefs != null) {
7016 callback.onPreferencesFetched(prefs);
7017 } else {
7018 callback.onPreferencesFetchFailed();
7019 }
7020 });
7021 }
7022
7023 public PushManagementService getPushManagementService() {
7024 return mPushManagementService;
7025 }
7026
7027 public void changeStatus(Account account, PresenceTemplate template, String signature) {
7028 if (!template.getStatusMessage().isEmpty()) {
7029 databaseBackend.insertPresenceTemplate(template);
7030 }
7031 account.setPgpSignature(signature);
7032 account.setPresenceStatus(template.getStatus());
7033 account.setPresenceStatusMessage(template.getStatusMessage());
7034 databaseBackend.updateAccount(account);
7035 sendPresence(account);
7036 }
7037
7038 public List<PresenceTemplate> getPresenceTemplates(Account account) {
7039 List<PresenceTemplate> templates = databaseBackend.getPresenceTemplates();
7040 for (PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) {
7041 if (!templates.contains(template)) {
7042 templates.add(0, template);
7043 }
7044 }
7045 return templates;
7046 }
7047
7048 public void saveConversationAsBookmark(final Conversation conversation, final String name) {
7049 final Account account = conversation.getAccount();
7050 final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid());
7051 String nick = conversation.getMucOptions().getActualNick();
7052 if (nick == null) nick = conversation.getJid().getResource();
7053 if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
7054 bookmark.setNick(nick);
7055 }
7056 if (!TextUtils.isEmpty(name)) {
7057 bookmark.setBookmarkName(name);
7058 }
7059 bookmark.setAutojoin(true);
7060 createBookmark(account, bookmark);
7061 bookmark.setConversation(conversation);
7062 }
7063
7064 public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
7065 boolean performedVerification = false;
7066 final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
7067 for (XmppUri.Fingerprint fp : fingerprints) {
7068 if (fp.type == XmppUri.FingerprintType.OMEMO) {
7069 String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
7070 FingerprintStatus fingerprintStatus =
7071 axolotlService.getFingerprintTrust(fingerprint);
7072 if (fingerprintStatus != null) {
7073 if (!fingerprintStatus.isVerified()) {
7074 performedVerification = true;
7075 axolotlService.setFingerprintTrust(
7076 fingerprint, fingerprintStatus.toVerified());
7077 }
7078 } else {
7079 axolotlService.preVerifyFingerprint(contact, fingerprint);
7080 }
7081 }
7082 }
7083 return performedVerification;
7084 }
7085
7086 public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
7087 final AxolotlService axolotlService = account.getAxolotlService();
7088 boolean verifiedSomething = false;
7089 for (XmppUri.Fingerprint fp : fingerprints) {
7090 if (fp.type == XmppUri.FingerprintType.OMEMO) {
7091 String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
7092 Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint);
7093 FingerprintStatus fingerprintStatus =
7094 axolotlService.getFingerprintTrust(fingerprint);
7095 if (fingerprintStatus != null) {
7096 if (!fingerprintStatus.isVerified()) {
7097 axolotlService.setFingerprintTrust(
7098 fingerprint, fingerprintStatus.toVerified());
7099 verifiedSomething = true;
7100 }
7101 } else {
7102 axolotlService.preVerifyFingerprint(account, fingerprint);
7103 verifiedSomething = true;
7104 }
7105 }
7106 }
7107 return verifiedSomething;
7108 }
7109
7110 public ShortcutService getShortcutService() {
7111 return mShortcutService;
7112 }
7113
7114 public void pushMamPreferences(Account account, Element prefs) {
7115 final Iq set = new Iq(Iq.Type.SET);
7116 set.addChild(prefs);
7117 account.setMamPrefs(prefs);
7118 sendIqPacket(account, set, null);
7119 }
7120
7121 public void evictPreview(File f) {
7122 if (f == null) return;
7123
7124 if (mDrawableCache.remove(f.getAbsolutePath()) != null) {
7125 Log.d(Config.LOGTAG, "deleted cached preview");
7126 }
7127 }
7128
7129 public void evictPreview(String uuid) {
7130 if (mDrawableCache.remove(uuid) != null) {
7131 Log.d(Config.LOGTAG, "deleted cached preview");
7132 }
7133 }
7134
7135 public interface OnMamPreferencesFetched {
7136 void onPreferencesFetched(Element prefs);
7137
7138 void onPreferencesFetchFailed();
7139 }
7140
7141 public interface OnAccountCreated {
7142 void onAccountCreated(Account account);
7143
7144 void informUser(int r);
7145 }
7146
7147 public interface OnMoreMessagesLoaded {
7148 void onMoreMessagesLoaded(int count, Conversation conversation);
7149
7150 void informUser(int r);
7151 }
7152
7153 public interface OnAccountPasswordChanged {
7154 void onPasswordChangeSucceeded();
7155
7156 void onPasswordChangeFailed();
7157 }
7158
7159 public interface OnRoomDestroy {
7160 void onRoomDestroySucceeded();
7161
7162 void onRoomDestroyFailed();
7163 }
7164
7165 public interface OnAffiliationChanged {
7166 void onAffiliationChangedSuccessful(Jid jid);
7167
7168 void onAffiliationChangeFailed(Jid jid, int resId);
7169 }
7170
7171 public interface OnConversationUpdate {
7172 default void onConversationUpdate() { onConversationUpdate(false); }
7173 default void onConversationUpdate(boolean newCaps) { onConversationUpdate(); }
7174 }
7175
7176 public interface OnJingleRtpConnectionUpdate {
7177 void onJingleRtpConnectionUpdate(
7178 final Account account,
7179 final Jid with,
7180 final String sessionId,
7181 final RtpEndUserState state);
7182
7183 void onAudioDeviceChanged(
7184 CallIntegration.AudioDevice selectedAudioDevice,
7185 Set<CallIntegration.AudioDevice> availableAudioDevices);
7186 }
7187
7188 public interface OnAccountUpdate {
7189 void onAccountUpdate();
7190 }
7191
7192 public interface OnCaptchaRequested {
7193 void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha);
7194 }
7195
7196 public interface OnRosterUpdate {
7197 void onRosterUpdate(final UpdateRosterReason reason, final Contact contact);
7198 }
7199
7200 public interface OnMucRosterUpdate {
7201 void onMucRosterUpdate();
7202 }
7203
7204 public interface OnConferenceConfigurationFetched {
7205 void onConferenceConfigurationFetched(Conversation conversation);
7206
7207 void onFetchFailed(Conversation conversation, String errorCondition);
7208 }
7209
7210 public interface OnConferenceJoined {
7211 void onConferenceJoined(Conversation conversation);
7212 }
7213
7214 public interface OnConfigurationPushed {
7215 void onPushSucceeded();
7216
7217 void onPushFailed();
7218 }
7219
7220 public interface OnShowErrorToast {
7221 void onShowErrorToast(int resId);
7222 }
7223
7224 public class XmppConnectionBinder extends Binder {
7225 public XmppConnectionService getService() {
7226 return XmppConnectionService.this;
7227 }
7228 }
7229
7230 private class InternalEventReceiver extends BroadcastReceiver {
7231
7232 @Override
7233 public void onReceive(final Context context, final Intent intent) {
7234 onStartCommand(intent, 0, 0);
7235 }
7236 }
7237
7238 private class RestrictedEventReceiver extends BroadcastReceiver {
7239
7240 private final Collection<String> allowedActions;
7241
7242 private RestrictedEventReceiver(final Collection<String> allowedActions) {
7243 this.allowedActions = allowedActions;
7244 }
7245
7246 @Override
7247 public void onReceive(final Context context, final Intent intent) {
7248 final String action = intent == null ? null : intent.getAction();
7249 if (allowedActions.contains(action)) {
7250 onStartCommand(intent, 0, 0);
7251 } else {
7252 Log.e(Config.LOGTAG, "restricting broadcast of event " + action);
7253 }
7254 }
7255 }
7256
7257 public static class OngoingCall {
7258 public final AbstractJingleConnection.Id id;
7259 public final Set<Media> media;
7260 public final boolean reconnecting;
7261
7262 public OngoingCall(
7263 AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
7264 this.id = id;
7265 this.media = media;
7266 this.reconnecting = reconnecting;
7267 }
7268
7269 @Override
7270 public boolean equals(Object o) {
7271 if (this == o) return true;
7272 if (o == null || getClass() != o.getClass()) return false;
7273 OngoingCall that = (OngoingCall) o;
7274 return reconnecting == that.reconnecting
7275 && Objects.equal(id, that.id)
7276 && Objects.equal(media, that.media);
7277 }
7278
7279 @Override
7280 public int hashCode() {
7281 return Objects.hashCode(id, media, reconnecting);
7282 }
7283 }
7284
7285 public static void toggleForegroundService(final XmppConnectionService service) {
7286 if (service == null) {
7287 return;
7288 }
7289 service.toggleForegroundService();
7290 }
7291
7292 public static void toggleForegroundService(final ConversationsActivity activity) {
7293 if (activity == null) {
7294 return;
7295 }
7296 toggleForegroundService(activity.xmppConnectionService);
7297 }
7298
7299 public static class BlockedMediaException extends Exception { }
7300
7301 public static enum UpdateRosterReason {
7302 INIT,
7303 AVATAR,
7304 PUSH,
7305 PRESENCE
7306 }
7307}