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