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