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