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