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