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(final Message message, final boolean delay, final Runnable cb) {
2131 Log.d(Config.LOGTAG, "send file message");
2132 final Account account = message.getConversation().getAccount();
2133 if (account.httpUploadAvailable(fileBackend.getFile(message, false).getSize())
2134 || message.getConversation().getMode() == Conversation.MODE_MULTI) {
2135 mHttpConnectionManager.createNewUploadConnection(message, delay, cb);
2136 } else {
2137 mJingleConnectionManager.startJingleFileTransfer(message);
2138 if (cb != null) cb.run();
2139 }
2140 }
2141
2142 public void sendMessage(final Message message) {
2143 sendMessage(message, false, false, false, null);
2144 }
2145
2146 public void sendMessage(final Message message, final Runnable cb) {
2147 sendMessage(message, false, false, false, cb);
2148 }
2149
2150 private void sendMessage(final Message message, final boolean resend, final boolean previewedLinks, final boolean delay, final Runnable cb) {
2151 final Account account = message.getConversation().getAccount();
2152 if (account.setShowErrorNotification(true)) {
2153 databaseBackend.updateAccount(account);
2154 mNotificationService.updateErrorNotification();
2155 }
2156 final Conversation conversation = (Conversation) message.getConversation();
2157 account.deactivateGracePeriod();
2158
2159 if (QuickConversationsService.isQuicksy()
2160 && conversation.getMode() == Conversation.MODE_SINGLE) {
2161 final Contact contact = conversation.getContact();
2162 if (!contact.showInRoster() && contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) {
2163 Log.d(
2164 Config.LOGTAG,
2165 account.getJid().asBareJid()
2166 + ": adding "
2167 + contact.getJid()
2168 + " on sending message");
2169 createContact(contact, true);
2170 }
2171 }
2172
2173 im.conversations.android.xmpp.model.stanza.Message packet = null;
2174 final boolean addToConversation = !message.edited() && message.getRawBody() != null;
2175 boolean saveInDb = addToConversation;
2176 message.setStatus(Message.STATUS_WAITING);
2177
2178 if (message.getEncryption() != Message.ENCRYPTION_NONE
2179 && conversation.getMode() == Conversation.MODE_MULTI
2180 && conversation.isPrivateAndNonAnonymous()) {
2181 if (conversation.setAttribute(
2182 Conversation.ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, true)) {
2183 databaseBackend.updateConversation(conversation);
2184 }
2185 }
2186
2187 final boolean inProgressJoin = isJoinInProgress(conversation);
2188
2189 if (message.getCounterpart() == null && !message.isPrivateMessage()) {
2190 message.setCounterpart(message.getConversation().getJid().asBareJid());
2191 }
2192
2193 boolean waitForPreview = false;
2194 if (getPreferences().getBoolean("send_link_previews", true) && !previewedLinks && !message.needsUploading() && message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
2195 message.clearLinkDescriptions();
2196 final List<URI> links = message.getLinks();
2197 if (!links.isEmpty()) {
2198 waitForPreview = true;
2199 if (account.isOnlineAndConnected()) {
2200 FILE_ATTACHMENT_EXECUTOR.execute(() -> {
2201 for (URI link : links) {
2202 if ("https".equals(link.getScheme())) {
2203 try {
2204 HttpUrl url = HttpUrl.parse(link.toString());
2205 OkHttpClient http = getHttpConnectionManager().buildHttpClient(url, account, 5, false);
2206 final var request = new okhttp3.Request.Builder().url(url).head().build();
2207 okhttp3.Response response = null;
2208 if ("www.amazon.com".equals(link.getHost()) || "www.amazon.ca".equals(link.getHost())) {
2209 // Amazon blocks HEAD
2210 response = new okhttp3.Response.Builder().request(request).protocol(okhttp3.Protocol.HTTP_1_1).code(200).message("OK").addHeader("Content-Type", "text/html").build();
2211 } else {
2212 response = http.newCall(request).execute();
2213 }
2214 final String mimeType = response.header("Content-Type") == null ? "" : response.header("Content-Type");
2215 final boolean image = mimeType.startsWith("image/");
2216 final boolean audio = mimeType.startsWith("audio/");
2217 final boolean video = mimeType.startsWith("video/");
2218 final boolean pdf = mimeType.equals("application/pdf");
2219 final boolean html = mimeType.startsWith("text/html") || mimeType.startsWith("application/xhtml+xml");
2220 if (response.isSuccessful() && (image || audio || video || pdf)) {
2221 Message.FileParams params = message.getFileParams();
2222 params.url = url.toString();
2223 if (response.header("Content-Length") != null) params.size = Long.parseLong(response.header("Content-Length"), 10);
2224 if (!Message.configurePrivateFileMessage(message)) {
2225 message.setType(image ? Message.TYPE_IMAGE : Message.TYPE_FILE);
2226 }
2227 params.setName(HttpConnectionManager.extractFilenameFromResponse(response));
2228
2229 if (link.toString().equals(message.getRawBody())) {
2230 Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
2231 fallback.addChild("body", "urn:xmpp:fallback:0");
2232 message.addPayload(fallback);
2233 } else if (message.getRawBody().indexOf(link.toString()) >= 0) {
2234 // Part of the real body, not just a fallback
2235 Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", Namespace.OOB);
2236 fallback.addChild("body", "urn:xmpp:fallback:0")
2237 .setAttribute("start", "0")
2238 .setAttribute("end", "0");
2239 message.addPayload(fallback);
2240 }
2241
2242 final int encryption = message.getEncryption();
2243 getHttpConnectionManager().createNewDownloadConnection(message, false, (file) -> {
2244 message.setEncryption(encryption);
2245 synchronized (message.getConversation()) {
2246 if (message.getStatus() == Message.STATUS_WAITING) sendMessage(message, true, true, false, cb);
2247 }
2248 });
2249 return;
2250 } else if (response.isSuccessful() && html) {
2251 Semaphore waiter = new Semaphore(0);
2252 OpenGraphParser.Builder openGraphBuilder = new OpenGraphParser.Builder(new OpenGraphCallback() {
2253 @Override
2254 public void onPostResponse(OpenGraphResult result) {
2255 Element rdf = new Element("Description", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
2256 rdf.setAttribute("xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
2257 rdf.setAttribute("rdf:about", link.toString());
2258 if (result.getTitle() != null && !"".equals(result.getTitle())) {
2259 rdf.addChild("title", "https://ogp.me/ns#").setContent(result.getTitle());
2260 }
2261 if (result.getDescription() != null && !"".equals(result.getDescription())) {
2262 rdf.addChild("description", "https://ogp.me/ns#").setContent(result.getDescription());
2263 }
2264 if (result.getUrl() != null) {
2265 rdf.addChild("url", "https://ogp.me/ns#").setContent(result.getUrl());
2266 }
2267 if (result.getImage() != null) {
2268 rdf.addChild("image", "https://ogp.me/ns#").setContent(result.getImage());
2269 }
2270 if (result.getType() != null) {
2271 rdf.addChild("type", "https://ogp.me/ns#").setContent(result.getType());
2272 }
2273 if (result.getSiteName() != null) {
2274 rdf.addChild("site_name", "https://ogp.me/ns#").setContent(result.getSiteName());
2275 }
2276 if (result.getVideo() != null) {
2277 rdf.addChild("video", "https://ogp.me/ns#").setContent(result.getVideo());
2278 }
2279 message.addPayload(rdf);
2280 waiter.release();
2281 }
2282
2283 public void onError(String error) {
2284 waiter.release();
2285 }
2286 })
2287 .showNullOnEmpty(true)
2288 .maxBodySize(90000)
2289 .timeout(5000);
2290 if (useTorToConnect()) {
2291 openGraphBuilder = openGraphBuilder.jsoupProxy(new JsoupProxy("127.0.0.1", 8118));
2292 }
2293 openGraphBuilder.build().parse(link.toString());
2294 waiter.tryAcquire(10L, TimeUnit.SECONDS);
2295 }
2296 } catch (final IOException | InterruptedException e) { }
2297 }
2298 }
2299 synchronized (message.getConversation()) {
2300 if (message.getStatus() == Message.STATUS_WAITING) sendMessage(message, true, true, false, cb);
2301 }
2302 });
2303 }
2304 }
2305 }
2306
2307 boolean passedCbOn = false;
2308 if (account.isOnlineAndConnected() && !inProgressJoin && !waitForPreview && message.getTimeSent() <= System.currentTimeMillis()) {
2309 switch (message.getEncryption()) {
2310 case Message.ENCRYPTION_NONE:
2311 if (message.needsUploading()) {
2312 if (account.httpUploadAvailable(
2313 fileBackend.getFile(message, false).getSize())
2314 || conversation.getMode() == Conversation.MODE_MULTI
2315 || message.fixCounterpart()) {
2316 this.sendFileMessage(message, delay, cb);
2317 passedCbOn = true;
2318 } else {
2319 break;
2320 }
2321 } else {
2322 packet = mMessageGenerator.generateChat(message);
2323 }
2324 break;
2325 case Message.ENCRYPTION_PGP:
2326 case Message.ENCRYPTION_DECRYPTED:
2327 if (message.needsUploading()) {
2328 if (account.httpUploadAvailable(
2329 fileBackend.getFile(message, false).getSize())
2330 || conversation.getMode() == Conversation.MODE_MULTI
2331 || message.fixCounterpart()) {
2332 this.sendFileMessage(message, delay, cb);
2333 passedCbOn = true;
2334 } else {
2335 break;
2336 }
2337 } else {
2338 packet = mMessageGenerator.generatePgpChat(message);
2339 }
2340 break;
2341 case Message.ENCRYPTION_AXOLOTL:
2342 message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
2343 if (message.needsUploading()) {
2344 if (account.httpUploadAvailable(
2345 fileBackend.getFile(message, false).getSize())
2346 || conversation.getMode() == Conversation.MODE_MULTI
2347 || message.fixCounterpart()) {
2348 this.sendFileMessage(message, delay, cb);
2349 passedCbOn = true;
2350 } else {
2351 break;
2352 }
2353 } else {
2354 XmppAxolotlMessage axolotlMessage =
2355 account.getAxolotlService().fetchAxolotlMessageFromCache(message);
2356 if (axolotlMessage == null) {
2357 account.getAxolotlService().preparePayloadMessage(message, delay);
2358 } else {
2359 packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
2360 }
2361 }
2362 break;
2363 }
2364 if (packet != null) {
2365 if (account.getXmppConnection().getFeatures().sm()
2366 || (conversation.getMode() == Conversation.MODE_MULTI
2367 && message.getCounterpart().isBareJid())) {
2368 message.setStatus(Message.STATUS_UNSEND);
2369 } else {
2370 message.setStatus(Message.STATUS_SEND);
2371 }
2372 }
2373 } else {
2374 switch (message.getEncryption()) {
2375 case Message.ENCRYPTION_DECRYPTED:
2376 if (!message.needsUploading()) {
2377 String pgpBody = message.getEncryptedBody();
2378 String decryptedBody = message.getBody();
2379 message.setBody(pgpBody); // TODO might throw NPE
2380 message.setEncryption(Message.ENCRYPTION_PGP);
2381 if (message.edited()) {
2382 message.setBody(decryptedBody);
2383 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
2384 if (!databaseBackend.updateMessage(message, message.getEditedId())) {
2385 Log.e(Config.LOGTAG, "error updated message in DB after edit");
2386 }
2387 updateConversationUi();
2388 if (!waitForPreview && cb != null) cb.run();
2389 return;
2390 } else {
2391 databaseBackend.createMessage(message);
2392 saveInDb = false;
2393 message.setBody(decryptedBody);
2394 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
2395 }
2396 }
2397 break;
2398 case Message.ENCRYPTION_AXOLOTL:
2399 message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
2400 break;
2401 }
2402 }
2403
2404 synchronized (mScheduledMessages) {
2405 if (message.getTimeSent() > System.currentTimeMillis()) {
2406 mScheduledMessages.put(message.getUuid(), message);
2407 scheduleNextIdlePing();
2408 } else {
2409 mScheduledMessages.remove(message.getUuid());
2410 }
2411 }
2412
2413 boolean mucMessage =
2414 conversation.getMode() == Conversation.MODE_MULTI && !message.isPrivateMessage();
2415 if (mucMessage) {
2416 message.setCounterpart(conversation.getMucOptions().getSelf().getFullJid());
2417 }
2418
2419 if (resend) {
2420 if (packet != null && addToConversation) {
2421 if (account.getXmppConnection().getFeatures().sm() || mucMessage) {
2422 markMessage(message, Message.STATUS_UNSEND);
2423 } else {
2424 markMessage(message, Message.STATUS_SEND);
2425 }
2426 }
2427 } else {
2428 if (addToConversation) {
2429 conversation.add(message);
2430 }
2431 if (saveInDb) {
2432 databaseBackend.createMessage(message);
2433 } else if (message.edited()) {
2434 if (!databaseBackend.updateMessage(message, message.getEditedId())) {
2435 Log.e(Config.LOGTAG, "error updated message in DB after edit");
2436 }
2437 }
2438 updateConversationUi();
2439 }
2440 if (packet != null) {
2441 if (delay) {
2442 mMessageGenerator.addDelay(packet, message.getTimeSent());
2443 }
2444 if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
2445 if (this.sendChatStates()) {
2446 packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
2447 }
2448 }
2449 sendMessagePacket(account, packet);
2450 if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.hasCustomEmoji()) {
2451 if (message.getConversation() instanceof Conversation) presenceToMuc((Conversation) message.getConversation());
2452 }
2453 }
2454 if (!waitForPreview && !passedCbOn && cb != null) cb.run();
2455 }
2456
2457 private boolean isJoinInProgress(final Conversation conversation) {
2458 final Account account = conversation.getAccount();
2459 synchronized (account.inProgressConferenceJoins) {
2460 if (conversation.getMode() == Conversational.MODE_MULTI) {
2461 final boolean inProgress = account.inProgressConferenceJoins.contains(conversation);
2462 final boolean pending = account.pendingConferenceJoins.contains(conversation);
2463 final boolean inProgressJoin = inProgress || pending;
2464 if (inProgressJoin) {
2465 Log.d(
2466 Config.LOGTAG,
2467 account.getJid().asBareJid()
2468 + ": holding back message to group. inProgress="
2469 + inProgress
2470 + ", pending="
2471 + pending);
2472 }
2473 return inProgressJoin;
2474 } else {
2475 return false;
2476 }
2477 }
2478 }
2479
2480 private void sendUnsentMessages(final Conversation conversation) {
2481 synchronized (conversation) {
2482 conversation.findWaitingMessages(message -> resendMessage(message, true));
2483 }
2484 }
2485
2486 public void resendMessage(final Message message, final boolean delay) {
2487 sendMessage(message, true, false, delay, null);
2488 }
2489
2490 public void resendMessage(final Message message, final boolean delay, final Runnable cb) {
2491 sendMessage(message, true, false, delay, cb);
2492 }
2493
2494 public void resendMessage(final Message message, final boolean delay, final boolean previewedLinks) {
2495 sendMessage(message, true, previewedLinks, delay, null);
2496 }
2497
2498 public Pair<Account,Account> onboardingIncomplete() {
2499 if (getAccounts().size() != 2) return null;
2500 Account onboarding = null;
2501 Account newAccount = null;
2502 for (final Account account : getAccounts()) {
2503 if (account.getJid().getDomain().equals(Config.ONBOARDING_DOMAIN)) {
2504 onboarding = account;
2505 } else {
2506 newAccount = account;
2507 }
2508 }
2509
2510 if (onboarding != null && newAccount != null) {
2511 return new Pair<>(onboarding, newAccount);
2512 }
2513
2514 return null;
2515 }
2516
2517 public boolean isOnboarding() {
2518 return getAccounts().size() == 1 && getAccounts().get(0).getJid().getDomain().equals(Config.ONBOARDING_DOMAIN);
2519 }
2520
2521 public void requestEasyOnboardingInvite(
2522 final Account account, final EasyOnboardingInvite.OnInviteRequested callback) {
2523 final XmppConnection connection = account.getXmppConnection();
2524 final Jid jid =
2525 connection == null
2526 ? null
2527 : connection.getJidForCommand(Namespace.EASY_ONBOARDING_INVITE);
2528 if (jid == null) {
2529 callback.inviteRequestFailed(
2530 getString(R.string.server_does_not_support_easy_onboarding_invites));
2531 return;
2532 }
2533 final Iq request = new Iq(Iq.Type.SET);
2534 request.setTo(jid);
2535 final Element command = request.addChild("command", Namespace.COMMANDS);
2536 command.setAttribute("node", Namespace.EASY_ONBOARDING_INVITE);
2537 command.setAttribute("action", "execute");
2538 sendIqPacket(
2539 account,
2540 request,
2541 (response) -> {
2542 if (response.getType() == Iq.Type.RESULT) {
2543 final Element resultCommand =
2544 response.findChild("command", Namespace.COMMANDS);
2545 final Element x =
2546 resultCommand == null
2547 ? null
2548 : resultCommand.findChild("x", Namespace.DATA);
2549 if (x != null) {
2550 final Data data = Data.parse(x);
2551 final String uri = data.getValue("uri");
2552 final String landingUrl = data.getValue("landing-url");
2553 if (uri != null) {
2554 final EasyOnboardingInvite invite =
2555 new EasyOnboardingInvite(
2556 jid.getDomain().toString(), uri, landingUrl);
2557 callback.inviteRequested(invite);
2558 return;
2559 }
2560 }
2561 callback.inviteRequestFailed(getString(R.string.unable_to_parse_invite));
2562 Log.d(Config.LOGTAG, response.toString());
2563 } else if (response.getType() == Iq.Type.ERROR) {
2564 callback.inviteRequestFailed(IqParser.errorMessage(response));
2565 } else {
2566 callback.inviteRequestFailed(getString(R.string.remote_server_timeout));
2567 }
2568 });
2569 }
2570
2571 public void fetchBookmarks(final Account account) {
2572 final Iq iqPacket = new Iq(Iq.Type.GET);
2573 final Element query = iqPacket.query("jabber:iq:private");
2574 query.addChild("storage", Namespace.BOOKMARKS);
2575 final Consumer<Iq> callback =
2576 (response) -> {
2577 if (response.getType() == Iq.Type.RESULT) {
2578 final Element query1 = response.query();
2579 final Element storage = query1.findChild("storage", "storage:bookmarks");
2580 Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
2581 processBookmarksInitial(account, bookmarks, false);
2582 } else {
2583 Log.d(
2584 Config.LOGTAG,
2585 account.getJid().asBareJid() + ": could not fetch bookmarks");
2586 }
2587 };
2588 sendIqPacket(account, iqPacket, callback);
2589 }
2590
2591 public void fetchBookmarks2(final Account account) {
2592 final Iq retrieve = mIqGenerator.retrieveBookmarks();
2593 sendIqPacket(
2594 account,
2595 retrieve,
2596 (response) -> {
2597 if (response.getType() == Iq.Type.RESULT) {
2598 final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB);
2599 final Map<Jid, Bookmark> bookmarks =
2600 Bookmark.parseFromPubSub(pubsub, account);
2601 processBookmarksInitial(account, bookmarks, true);
2602 }
2603 });
2604 }
2605
2606 public void fetchMessageDisplayedSynchronization(final Account account) {
2607 Log.d(Config.LOGTAG, account.getJid() + ": retrieve mds");
2608 final var retrieve = mIqGenerator.retrieveMds();
2609 sendIqPacket(
2610 account,
2611 retrieve,
2612 (response) -> {
2613 if (response.getType() != Iq.Type.RESULT) {
2614 return;
2615 }
2616 final var pubSub = response.findChild("pubsub", Namespace.PUBSUB);
2617 final Element items = pubSub == null ? null : pubSub.findChild("items");
2618 if (items == null
2619 || !Namespace.MDS_DISPLAYED.equals(items.getAttribute("node"))) {
2620 return;
2621 }
2622 for (final Element child : items.getChildren()) {
2623 if ("item".equals(child.getName())) {
2624 processMdsItem(account, child);
2625 }
2626 }
2627 });
2628 }
2629
2630 public void processMdsItem(final Account account, final Element item) {
2631 final Jid jid =
2632 item == null ? null : Jid.Invalid.getNullForInvalid(item.getAttributeAsJid("id"));
2633 if (jid == null) {
2634 return;
2635 }
2636 final Element displayed = item.findChild("displayed", Namespace.MDS_DISPLAYED);
2637 final Element stanzaId =
2638 displayed == null ? null : displayed.findChild("stanza-id", Namespace.STANZA_IDS);
2639 final String id = stanzaId == null ? null : stanzaId.getAttribute("id");
2640 final Conversation conversation = find(account, jid);
2641 if (id != null && conversation != null) {
2642 conversation.setDisplayState(id);
2643 markReadUpToStanzaId(conversation, id);
2644 }
2645 }
2646
2647 public void markReadUpToStanzaId(final Conversation conversation, final String stanzaId) {
2648 final Message message = conversation.findMessageWithServerMsgId(stanzaId);
2649 if (message == null) { // do we want to check if isRead?
2650 return;
2651 }
2652 markReadUpTo(conversation, message);
2653 }
2654
2655 public void markReadUpTo(final Conversation conversation, final Message message) {
2656 final boolean isDismissNotification = isDismissNotification(message);
2657 final var uuid = message.getUuid();
2658 Log.d(
2659 Config.LOGTAG,
2660 conversation.getAccount().getJid().asBareJid()
2661 + ": mark "
2662 + conversation.getJid().asBareJid()
2663 + " as read up to "
2664 + uuid);
2665 markRead(conversation, uuid, isDismissNotification);
2666 }
2667
2668 private static boolean isDismissNotification(final Message message) {
2669 Message next = message.next();
2670 while (next != null) {
2671 if (message.getStatus() == Message.STATUS_RECEIVED) {
2672 return false;
2673 }
2674 next = next.next();
2675 }
2676 return true;
2677 }
2678
2679 public void processBookmarksInitial(
2680 final Account account, final Map<Jid, Bookmark> bookmarks, final boolean pep) {
2681 final Set<Jid> previousBookmarks = account.getBookmarkedJids();
2682 for (final Bookmark bookmark : bookmarks.values()) {
2683 previousBookmarks.remove(bookmark.getJid().asBareJid());
2684 processModifiedBookmark(bookmark, pep);
2685 }
2686 if (pep) {
2687 processDeletedBookmarks(account, previousBookmarks);
2688 }
2689 account.setBookmarks(bookmarks);
2690 }
2691
2692 public void processDeletedBookmarks(final Account account, final Collection<Jid> bookmarks) {
2693 Log.d(
2694 Config.LOGTAG,
2695 account.getJid().asBareJid()
2696 + ": "
2697 + bookmarks.size()
2698 + " bookmarks have been removed");
2699 for (final Jid bookmark : bookmarks) {
2700 processDeletedBookmark(account, bookmark);
2701 }
2702 }
2703
2704 public void processDeletedBookmark(final Account account, final Jid jid) {
2705 final Conversation conversation = find(account, jid);
2706 if (conversation == null) {
2707 return;
2708 }
2709 Log.d(
2710 Config.LOGTAG,
2711 account.getJid().asBareJid() + ": archiving MUC " + jid + " after PEP update");
2712 archiveConversation(conversation, false);
2713 }
2714
2715 private void processModifiedBookmark(final Bookmark bookmark, final boolean pep) {
2716 final Account account = bookmark.getAccount();
2717 Conversation conversation = find(bookmark);
2718 if (conversation != null) {
2719 if (conversation.getMode() != Conversation.MODE_MULTI) {
2720 return;
2721 }
2722 bookmark.setConversation(conversation);
2723 if (pep && !bookmark.autojoin()) {
2724 Log.d(
2725 Config.LOGTAG,
2726 account.getJid().asBareJid()
2727 + ": archiving conference ("
2728 + conversation.getJid()
2729 + ") after receiving pep");
2730 archiveConversation(conversation, false);
2731 } else {
2732 final MucOptions mucOptions = conversation.getMucOptions();
2733 if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) {
2734 final String current = mucOptions.getActualNick();
2735 final String proposed = mucOptions.getProposedNickPure();
2736 if (current != null && !current.equals(proposed)) {
2737 Log.d(
2738 Config.LOGTAG,
2739 account.getJid().asBareJid()
2740 + ": proposed nick changed after bookmark push "
2741 + current
2742 + "->"
2743 + proposed);
2744 joinMuc(conversation);
2745 }
2746 } else {
2747 checkMucRequiresRename(conversation);
2748 }
2749 }
2750 } else if (bookmark.autojoin()) {
2751 conversation =
2752 findOrCreateConversation(account, bookmark.getFullJid(), true, true, false);
2753 bookmark.setConversation(conversation);
2754 }
2755 }
2756
2757 public void processModifiedBookmark(final Bookmark bookmark) {
2758 processModifiedBookmark(bookmark, true);
2759 }
2760
2761 public void ensureBookmarkIsAutoJoin(final Conversation conversation) {
2762 final var account = conversation.getAccount();
2763 final var existingBookmark = conversation.getBookmark();
2764 if (existingBookmark == null) {
2765 final var bookmark = new Bookmark(account, conversation.getJid().asBareJid());
2766 bookmark.setAutojoin(true);
2767 createBookmark(account, bookmark);
2768 } else {
2769 if (existingBookmark.autojoin()) {
2770 return;
2771 }
2772 existingBookmark.setAutojoin(true);
2773 createBookmark(account, existingBookmark);
2774 }
2775 }
2776
2777 public void createBookmark(final Account account, final Bookmark bookmark) {
2778 account.putBookmark(bookmark);
2779 final XmppConnection connection = account.getXmppConnection();
2780 if (connection == null) {
2781 Log.d(
2782 Config.LOGTAG,
2783 account.getJid().asBareJid() + ": no connection. ignoring bookmark creation");
2784 } else if (connection.getFeatures().bookmarks2()) {
2785 Log.d(
2786 Config.LOGTAG,
2787 account.getJid().asBareJid() + ": pushing bookmark via Bookmarks 2");
2788 final Element item = mIqGenerator.publishBookmarkItem(bookmark);
2789 pushNodeAndEnforcePublishOptions(
2790 account,
2791 Namespace.BOOKMARKS2,
2792 item,
2793 bookmark.getJid().asBareJid().toString(),
2794 PublishOptions.persistentWhitelistAccessMaxItems());
2795 } else if (connection.getFeatures().bookmarksConversion()) {
2796 pushBookmarksPep(account);
2797 } else {
2798 pushBookmarksPrivateXml(account);
2799 }
2800 }
2801
2802 public void deleteBookmark(final Account account, final Bookmark bookmark) {
2803 if (bookmark.getJid().toString().equals("discuss@conference.soprani.ca")) {
2804 getPreferences().edit().putBoolean("cheogram_sopranica_bookmark_deleted", true).apply();
2805 }
2806 account.removeBookmark(bookmark);
2807 final XmppConnection connection = account.getXmppConnection();
2808 if (connection == null) return;
2809
2810 if (connection.getFeatures().bookmarks2()) {
2811 final Iq request =
2812 mIqGenerator.deleteItem(
2813 Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toString());
2814 Log.d(
2815 Config.LOGTAG,
2816 account.getJid().asBareJid() + ": removing bookmark via Bookmarks 2");
2817 sendIqPacket(
2818 account,
2819 request,
2820 (response) -> {
2821 if (response.getType() == Iq.Type.ERROR) {
2822 Log.d(
2823 Config.LOGTAG,
2824 account.getJid().asBareJid()
2825 + ": unable to delete bookmark "
2826 + response.getErrorCondition());
2827 }
2828 });
2829 } else if (connection.getFeatures().bookmarksConversion()) {
2830 pushBookmarksPep(account);
2831 } else {
2832 pushBookmarksPrivateXml(account);
2833 }
2834 }
2835
2836 private void pushBookmarksPrivateXml(Account account) {
2837 if (!account.areBookmarksLoaded()) return;
2838
2839 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml");
2840 final Iq iqPacket = new Iq(Iq.Type.SET);
2841 Element query = iqPacket.query("jabber:iq:private");
2842 Element storage = query.addChild("storage", "storage:bookmarks");
2843 for (final Bookmark bookmark : account.getBookmarks()) {
2844 storage.addChild(bookmark);
2845 }
2846 sendIqPacket(account, iqPacket, mDefaultIqHandler);
2847 }
2848
2849 private void pushBookmarksPep(Account account) {
2850 if (!account.areBookmarksLoaded()) return;
2851
2852 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via pep");
2853 final Element storage = new Element("storage", "storage:bookmarks");
2854 for (final Bookmark bookmark : account.getBookmarks()) {
2855 storage.addChild(bookmark);
2856 }
2857 pushNodeAndEnforcePublishOptions(
2858 account,
2859 Namespace.BOOKMARKS,
2860 storage,
2861 "current",
2862 PublishOptions.persistentWhitelistAccess());
2863 }
2864
2865 private void pushNodeAndEnforcePublishOptions(
2866 final Account account,
2867 final String node,
2868 final Element element,
2869 final String id,
2870 final Bundle options) {
2871 pushNodeAndEnforcePublishOptions(account, node, element, id, options, true);
2872 }
2873
2874 private void pushNodeAndEnforcePublishOptions(
2875 final Account account,
2876 final String node,
2877 final Element element,
2878 final String id,
2879 final Bundle options,
2880 final boolean retry) {
2881 final Iq packet = mIqGenerator.publishElement(node, element, id, options);
2882 sendIqPacket(
2883 account,
2884 packet,
2885 (response) -> {
2886 if (response.getType() == Iq.Type.RESULT) {
2887 return;
2888 }
2889 if (retry && PublishOptions.preconditionNotMet(response)) {
2890 pushNodeConfiguration(
2891 account,
2892 node,
2893 options,
2894 new OnConfigurationPushed() {
2895 @Override
2896 public void onPushSucceeded() {
2897 pushNodeAndEnforcePublishOptions(
2898 account, node, element, id, options, false);
2899 }
2900
2901 @Override
2902 public void onPushFailed() {
2903 Log.d(
2904 Config.LOGTAG,
2905 account.getJid().asBareJid()
2906 + ": unable to push node configuration ("
2907 + node
2908 + ")");
2909 }
2910 });
2911 } else {
2912 Log.d(
2913 Config.LOGTAG,
2914 account.getJid().asBareJid()
2915 + ": error publishing "
2916 + node
2917 + " (retry="
2918 + retry
2919 + ") "
2920 + response);
2921 }
2922 });
2923 }
2924
2925 private void restoreFromDatabase() {
2926 synchronized (this.conversations) {
2927 final Map<String, Account> accountLookupTable =
2928 ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
2929 Log.d(Config.LOGTAG, "restoring conversations...");
2930 final long startTimeConversationsRestore = SystemClock.elapsedRealtime();
2931 this.conversations.addAll(
2932 databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
2933 for (Iterator<Conversation> iterator = conversations.listIterator();
2934 iterator.hasNext(); ) {
2935 Conversation conversation = iterator.next();
2936 Account account = accountLookupTable.get(conversation.getAccountUuid());
2937 if (account != null) {
2938 conversation.setAccount(account);
2939 } else {
2940 Log.e(Config.LOGTAG, "unable to restore Conversations with " + conversation.getJid());
2941 conversations.remove(conversation);
2942 }
2943 }
2944 long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore;
2945 Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms");
2946 Runnable runnable = () -> {
2947 if (DatabaseBackend.requiresMessageIndexRebuild()) {
2948 DatabaseBackend.getInstance(this).rebuildMessagesIndex();
2949 }
2950 mutedMucUsers = databaseBackend.loadMutedMucUsers();
2951 final long deletionDate = getAutomaticMessageDeletionDate();
2952 mLastExpiryRun.set(SystemClock.elapsedRealtime());
2953 if (deletionDate > 0) {
2954 Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate));
2955 databaseBackend.expireOldMessages(deletionDate);
2956 }
2957 Log.d(Config.LOGTAG, "restoring roster...");
2958 for (final Account account : accounts) {
2959 databaseBackend.readRoster(account.getRoster());
2960 account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage
2961 }
2962 getDrawableCache().evictAll();
2963 loadPhoneContacts();
2964 Log.d(Config.LOGTAG, "restoring messages...");
2965 final long startMessageRestore = SystemClock.elapsedRealtime();
2966 final Conversation quickLoad = QuickLoader.get(this.conversations);
2967 if (quickLoad != null) {
2968 restoreMessages(quickLoad);
2969 updateConversationUi();
2970 final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
2971 Log.d(Config.LOGTAG, "quickly restored " + quickLoad.getName() + " after " + diffMessageRestore + "ms");
2972 }
2973 for (Conversation conversation : this.conversations) {
2974 if (quickLoad != conversation) {
2975 restoreMessages(conversation);
2976 }
2977 }
2978 mNotificationService.finishBacklog();
2979 restoredFromDatabaseLatch.countDown();
2980 final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
2981 Log.d(Config.LOGTAG, "finished restoring messages in " + diffMessageRestore + "ms");
2982 updateConversationUi();
2983 };
2984 mDatabaseReaderExecutor.execute(runnable); //will contain one write command (expiry) but that's fine
2985 }
2986 }
2987
2988 private void restoreMessages(Conversation conversation) {
2989 conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
2990 conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
2991 conversation.findMessagesAndCallsToNotify(mNotificationService::pushFromBacklog);
2992 }
2993
2994 public void loadPhoneContacts() {
2995 mContactMergerExecutor.execute(
2996 () -> {
2997 final Map<Jid, JabberIdContact> contacts = JabberIdContact.load(this);
2998 Log.d(Config.LOGTAG, "start merging phone contacts with roster");
2999 for (final Account account : accounts) {
3000 final List<Contact> withSystemAccounts =
3001 account.getRoster().getWithSystemAccounts(JabberIdContact.class);
3002 for (final JabberIdContact jidContact : contacts.values()) {
3003 final Contact contact =
3004 account.getRoster().getContact(jidContact.getJid());
3005 boolean needsCacheClean = contact.setPhoneContact(jidContact);
3006 if (needsCacheClean) {
3007 getAvatarService().clear(contact);
3008 }
3009 withSystemAccounts.remove(contact);
3010 }
3011 for (final Contact contact : withSystemAccounts) {
3012 boolean needsCacheClean =
3013 contact.unsetPhoneContact(JabberIdContact.class);
3014 if (needsCacheClean) {
3015 getAvatarService().clear(contact);
3016 }
3017 }
3018 }
3019 Log.d(Config.LOGTAG, "finished merging phone contacts");
3020 mShortcutService.refresh(
3021 mInitialAddressbookSyncCompleted.compareAndSet(false, true));
3022 updateRosterUi(UpdateRosterReason.PUSH);
3023 mQuickConversationsService.considerSync();
3024 });
3025 }
3026
3027 public void syncRoster(final Account account) {
3028 mRosterSyncTaskManager.execute(account, () -> {
3029 unregisterPhoneAccounts(account);
3030 databaseBackend.writeRoster(account.getRoster());
3031 try { Thread.sleep(500); } catch (InterruptedException e) { }
3032 });
3033 }
3034
3035 public List<Conversation> getConversations() {
3036 return this.conversations;
3037 }
3038
3039 private void markFileDeleted(final File file) {
3040 synchronized (FILENAMES_TO_IGNORE_DELETION) {
3041 if (FILENAMES_TO_IGNORE_DELETION.remove(file.getAbsolutePath())) {
3042 Log.d(Config.LOGTAG, "ignored deletion of " + file.getAbsolutePath());
3043 return;
3044 }
3045 }
3046 final boolean isInternalFile = fileBackend.isInternalFile(file);
3047 final List<String> uuids = databaseBackend.markFileAsDeleted(file, isInternalFile);
3048 Log.d(
3049 Config.LOGTAG,
3050 "deleted file "
3051 + file.getAbsolutePath()
3052 + " internal="
3053 + isInternalFile
3054 + ", database hits="
3055 + uuids.size());
3056 markUuidsAsDeletedFiles(uuids);
3057 }
3058
3059 private void markUuidsAsDeletedFiles(List<String> uuids) {
3060 boolean deleted = false;
3061 for (Conversation conversation : getConversations()) {
3062 deleted |= conversation.markAsDeleted(uuids);
3063 }
3064 for (final String uuid : uuids) {
3065 evictPreview(uuid);
3066 }
3067 if (deleted) {
3068 updateConversationUi();
3069 }
3070 }
3071
3072 private void markChangedFiles(List<DatabaseBackend.FilePathInfo> infos) {
3073 boolean changed = false;
3074 for (Conversation conversation : getConversations()) {
3075 changed |= conversation.markAsChanged(infos);
3076 }
3077 if (changed) {
3078 updateConversationUi();
3079 }
3080 }
3081
3082 public void populateWithOrderedConversations(final List<Conversation> list) {
3083 populateWithOrderedConversations(list, true, true);
3084 }
3085
3086 public void populateWithOrderedConversations(
3087 final List<Conversation> list, final boolean includeNoFileUpload) {
3088 populateWithOrderedConversations(list, includeNoFileUpload, true);
3089 }
3090
3091 public void populateWithOrderedConversations(
3092 final List<Conversation> list, final boolean includeNoFileUpload, final boolean sort) {
3093 final List<String> orderedUuids;
3094 if (sort) {
3095 orderedUuids = null;
3096 } else {
3097 orderedUuids = new ArrayList<>();
3098 for (Conversation conversation : list) {
3099 orderedUuids.add(conversation.getUuid());
3100 }
3101 }
3102 list.clear();
3103 if (includeNoFileUpload) {
3104 list.addAll(getConversations());
3105 } else {
3106 for (Conversation conversation : getConversations()) {
3107 if (conversation.getMode() == Conversation.MODE_SINGLE
3108 || (conversation.getAccount().httpUploadAvailable()
3109 && conversation.getMucOptions().participating())) {
3110 list.add(conversation);
3111 }
3112 }
3113 }
3114 try {
3115 if (orderedUuids != null) {
3116 Collections.sort(
3117 list,
3118 (a, b) -> {
3119 final int indexA = orderedUuids.indexOf(a.getUuid());
3120 final int indexB = orderedUuids.indexOf(b.getUuid());
3121 if (indexA == -1 || indexB == -1 || indexA == indexB) {
3122 return a.compareTo(b);
3123 }
3124 return indexA - indexB;
3125 });
3126 } else {
3127 Collections.sort(list);
3128 }
3129 } catch (IllegalArgumentException e) {
3130 // ignore
3131 }
3132 }
3133
3134 public void loadMoreMessages(
3135 final Conversation conversation,
3136 final long timestamp,
3137 final OnMoreMessagesLoaded callback) {
3138 if (XmppConnectionService.this
3139 .getMessageArchiveService()
3140 .queryInProgress(conversation, callback)) {
3141 return;
3142 } else if (timestamp == 0) {
3143 return;
3144 }
3145 Log.d(
3146 Config.LOGTAG,
3147 "load more messages for "
3148 + conversation.getName()
3149 + " prior to "
3150 + MessageGenerator.getTimestamp(timestamp));
3151 final Runnable runnable =
3152 () -> {
3153 final Account account = conversation.getAccount();
3154 List<Message> messages =
3155 databaseBackend.getMessages(conversation, 50, timestamp);
3156 if (messages.size() > 0) {
3157 conversation.addAll(0, messages);
3158 callback.onMoreMessagesLoaded(messages.size(), conversation);
3159 } else if (conversation.hasMessagesLeftOnServer()
3160 && account.isOnlineAndConnected()
3161 && conversation.getLastClearHistory().getTimestamp() == 0) {
3162 final boolean mamAvailable;
3163 if (conversation.getMode() == Conversation.MODE_SINGLE) {
3164 mamAvailable =
3165 account.getXmppConnection().getFeatures().mam()
3166 && !conversation.getContact().isBlocked();
3167 } else {
3168 mamAvailable = conversation.getMucOptions().mamSupport();
3169 }
3170 if (mamAvailable) {
3171 MessageArchiveService.Query query =
3172 getMessageArchiveService()
3173 .query(
3174 conversation,
3175 new MamReference(0),
3176 timestamp,
3177 false);
3178 if (query != null) {
3179 query.setCallback(callback);
3180 callback.informUser(R.string.fetching_history_from_server);
3181 } else {
3182 callback.informUser(R.string.not_fetching_history_retention_period);
3183 }
3184 }
3185 }
3186 };
3187 mDatabaseReaderExecutor.execute(runnable);
3188 }
3189
3190 public List<Account> getAccounts() {
3191 return this.accounts;
3192 }
3193
3194 /**
3195 * This will find all conferences with the contact as member and also the conference that is the
3196 * contact (that 'fake' contact is used to store the avatar)
3197 */
3198 public List<Conversation> findAllConferencesWith(Contact contact) {
3199 final ArrayList<Conversation> results = new ArrayList<>();
3200 for (final Conversation c : conversations) {
3201 if (c.getMode() != Conversation.MODE_MULTI) {
3202 continue;
3203 }
3204 final MucOptions mucOptions = c.getMucOptions();
3205 if (c.getJid().asBareJid().equals(contact.getJid().asBareJid())
3206 || (mucOptions != null && mucOptions.isContactInRoom(contact))) {
3207 results.add(c);
3208 }
3209 }
3210 return results;
3211 }
3212
3213 public Conversation find(final Contact contact) {
3214 for (final Conversation conversation : this.conversations) {
3215 if (conversation.getContact() == contact) {
3216 return conversation;
3217 }
3218 }
3219 return null;
3220 }
3221
3222 public Conversation find(
3223 final Iterable<Conversation> haystack, final Account account, final Jid jid) {
3224 if (jid == null) {
3225 return null;
3226 }
3227 for (final Conversation conversation : haystack) {
3228 if ((account == null || conversation.getAccount() == account)
3229 && (conversation.getJid().asBareJid().equals(jid.asBareJid()))) {
3230 return conversation;
3231 }
3232 }
3233 return null;
3234 }
3235
3236 public boolean isConversationsListEmpty(final Conversation ignore) {
3237 synchronized (this.conversations) {
3238 final int size = this.conversations.size();
3239 return size == 0 || size == 1 && this.conversations.get(0) == ignore;
3240 }
3241 }
3242
3243 public boolean isConversationStillOpen(final Conversation conversation) {
3244 synchronized (this.conversations) {
3245 for (Conversation current : this.conversations) {
3246 if (current == conversation) {
3247 return true;
3248 }
3249 }
3250 }
3251 return false;
3252 }
3253
3254 public void maybeRegisterWithMuc(Conversation c, String nickArg) {
3255 final var nick = nickArg == null ? c.getMucOptions().getSelf().getFullJid().getResource() : nickArg;
3256 final var register = new Iq(Iq.Type.GET);
3257 register.query(Namespace.REGISTER);
3258 register.setTo(c.getJid().asBareJid());
3259 sendIqPacket(c.getAccount(), register, (response) -> {
3260 if (response.getType() == Iq.Type.RESULT) {
3261 final Element query = response.query(Namespace.REGISTER);
3262 String username = query.findChildContent("username", Namespace.REGISTER);
3263 if (username == null) username = query.findChildContent("nick", Namespace.REGISTER);
3264 if (username != null && username.equals(nick)) {
3265 // Already registered with this nick, done
3266 Log.d(Config.LOGTAG, "Already registered with " + c.getJid().asBareJid() + " as " + username);
3267 return;
3268 }
3269 Data form = Data.parse(query.findChild("x", Namespace.DATA));
3270 if (form != null) {
3271 final var field = form.getFieldByName("muc#register_roomnick");
3272 if (field != null && nick.equals(field.getValue())) {
3273 Log.d(Config.LOGTAG, "Already registered with " + c.getJid().asBareJid() + " as " + field.getValue());
3274 return;
3275 }
3276 }
3277 if (form == null || !"form".equals(form.getFormType()) || !form.getFields().stream().anyMatch(f -> f.isRequired() && !"muc#register_roomnick".equals(f.getFieldName()))) {
3278 // No form, result form, or no required fields other than nickname, let's just send nickname
3279 if (form == null || !"form".equals(form.getFormType())) {
3280 form = new Data();
3281 form.put("FORM_TYPE", "http://jabber.org/protocol/muc#register");
3282 }
3283 form.put("muc#register_roomnick", nick);
3284 form.submit();
3285 final var finish = new Iq(Iq.Type.SET);
3286 finish.query(Namespace.REGISTER).addChild(form);
3287 finish.setTo(c.getJid().asBareJid());
3288 sendIqPacket(c.getAccount(), finish, (response2) -> {
3289 if (response.getType() == Iq.Type.RESULT) {
3290 Log.w(Config.LOGTAG, "Success registering with channel " + c.getJid().asBareJid() + "/" + nick);
3291 } else {
3292 Log.w(Config.LOGTAG, "Error registering with channel: " + response2);
3293 }
3294 });
3295 } else {
3296 // TODO: offer registration form to user
3297 Log.d(Config.LOGTAG, "Complex registration form for " + c.getJid().asBareJid() + ": " + response);
3298 }
3299 } else {
3300 // We said maybe. Guess not
3301 Log.d(Config.LOGTAG, "Could not register with " + c.getJid().asBareJid() + ": " + response);
3302 }
3303 });
3304 }
3305
3306 public void deregisterWithMuc(Conversation c) {
3307 final Iq register = new Iq(Iq.Type.GET);
3308 register.query(Namespace.REGISTER).addChild("remove");
3309 register.setTo(c.getJid().asBareJid());
3310 sendIqPacket(c.getAccount(), register, (response) -> {
3311 if (response.getType() == Iq.Type.RESULT) {
3312 Log.d(Config.LOGTAG, "deregistered with " + c.getJid().asBareJid());
3313 } else {
3314 Log.w(Config.LOGTAG, "Could not deregister with " + c.getJid().asBareJid() + ": " + response);
3315 }
3316 });
3317 }
3318
3319 public Conversation findOrCreateConversation(
3320 Account account, Jid jid, boolean muc, final boolean async) {
3321 return this.findOrCreateConversation(account, jid, muc, false, async);
3322 }
3323
3324 public Conversation findOrCreateConversation(
3325 final Account account,
3326 final Jid jid,
3327 final boolean muc,
3328 final boolean joinAfterCreate,
3329 final boolean async) {
3330 return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, null, async, null);
3331 }
3332
3333 public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final MessageArchiveService.Query query, final boolean async) {
3334 return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, query, async, null);
3335 }
3336
3337 public Conversation findOrCreateConversation(
3338 final Account account,
3339 final Jid jid,
3340 final boolean muc,
3341 final boolean joinAfterCreate,
3342 final MessageArchiveService.Query query,
3343 final boolean async,
3344 final String password) {
3345 synchronized (this.conversations) {
3346 final var cached = find(account, jid);
3347 if (cached != null) {
3348 return cached;
3349 }
3350 final var existing = databaseBackend.findConversation(account, jid);
3351 final Conversation conversation;
3352 final boolean loadMessagesFromDb;
3353 if (existing != null) {
3354 conversation = existing;
3355 if (password != null) conversation.getMucOptions().setPassword(password);
3356 loadMessagesFromDb = restoreFromArchive(conversation, jid, muc);
3357 } else {
3358 String conversationName;
3359 final Contact contact = account.getRoster().getContact(jid);
3360 if (contact != null) {
3361 conversationName = contact.getDisplayName();
3362 } else {
3363 conversationName = jid.getLocal();
3364 }
3365 if (muc) {
3366 conversation =
3367 new Conversation(
3368 conversationName, account, jid, Conversation.MODE_MULTI);
3369 } else {
3370 conversation =
3371 new Conversation(
3372 conversationName,
3373 account,
3374 jid.asBareJid(),
3375 Conversation.MODE_SINGLE);
3376 }
3377 if (password != null) conversation.getMucOptions().setPassword(password);
3378 this.databaseBackend.createConversation(conversation);
3379 loadMessagesFromDb = false;
3380 }
3381 if (async) {
3382 mDatabaseReaderExecutor.execute(
3383 () ->
3384 postProcessConversation(
3385 conversation, loadMessagesFromDb, joinAfterCreate, query));
3386 } else {
3387 postProcessConversation(conversation, loadMessagesFromDb, joinAfterCreate, query);
3388 }
3389 this.conversations.add(conversation);
3390 updateConversationUi();
3391 return conversation;
3392 }
3393 }
3394
3395 public Conversation findConversationByUuidReliable(final String uuid) {
3396 final var cached = findConversationByUuid(uuid);
3397 if (cached != null) {
3398 return cached;
3399 }
3400 final var existing = databaseBackend.findConversation(uuid);
3401 if (existing == null) {
3402 return null;
3403 }
3404 Log.d(
3405 Config.LOGTAG,
3406 existing.getJid().asBareJid()
3407 + ": restoring conversation with "
3408 + existing.getJid()
3409 + " from DB");
3410 final Map<String, Account> accounts =
3411 ImmutableMap.copyOf(Maps.uniqueIndex(this.accounts, Account::getUuid));
3412 existing.setAccount(accounts.get(existing.getAccountUuid()));
3413 final var loadMessagesFromDb = restoreFromArchive(existing);
3414 mDatabaseReaderExecutor.execute(
3415 () ->
3416 postProcessConversation(
3417 existing,
3418 loadMessagesFromDb,
3419 existing.getMode() == Conversational.MODE_MULTI,
3420 null));
3421 this.conversations.add(existing);
3422 if (existing.getMode() == Conversational.MODE_MULTI) {
3423 ensureBookmarkIsAutoJoin(existing);
3424 }
3425 updateConversationUi();
3426 return existing;
3427 }
3428
3429 private boolean restoreFromArchive(
3430 final Conversation conversation, final Jid jid, final boolean muc) {
3431 if (muc) {
3432 conversation.setMode(Conversation.MODE_MULTI);
3433 conversation.setContactJid(jid);
3434 } else {
3435 conversation.setMode(Conversation.MODE_SINGLE);
3436 conversation.setContactJid(jid.asBareJid());
3437 }
3438 return restoreFromArchive(conversation);
3439 }
3440
3441 private boolean restoreFromArchive(final Conversation conversation) {
3442 conversation.setStatus(Conversation.STATUS_AVAILABLE);
3443 databaseBackend.updateConversation(conversation);
3444 return conversation.messagesLoaded.compareAndSet(true, false);
3445 }
3446
3447 private void postProcessConversation(
3448 final Conversation c,
3449 final boolean loadMessagesFromDb,
3450 final boolean joinAfterCreate,
3451 final MessageArchiveService.Query query) {
3452 final var singleMode = c.getMode() == Conversational.MODE_SINGLE;
3453 final var account = c.getAccount();
3454 if (loadMessagesFromDb) {
3455 c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE));
3456 updateConversationUi();
3457 c.messagesLoaded.set(true);
3458 }
3459 if (account.getXmppConnection() != null
3460 && !c.getContact().isBlocked()
3461 && account.getXmppConnection().getFeatures().mam()
3462 && singleMode) {
3463 if (query == null) {
3464 mMessageArchiveService.query(c);
3465 } else {
3466 if (query.getConversation() == null) {
3467 mMessageArchiveService.query(c, query.getStart(), query.isCatchup());
3468 }
3469 }
3470 }
3471 if (joinAfterCreate) {
3472 joinMuc(c);
3473 }
3474 }
3475
3476 public void archiveConversation(Conversation conversation) {
3477 archiveConversation(conversation, true);
3478 }
3479
3480 private void archiveConversation(
3481 Conversation conversation, final boolean maySynchronizeWithBookmarks) {
3482 if (isOnboarding()) return;
3483
3484 getNotificationService().clear(conversation);
3485 conversation.setStatus(Conversation.STATUS_ARCHIVED);
3486 conversation.setNextMessage(null);
3487 synchronized (this.conversations) {
3488 getMessageArchiveService().kill(conversation);
3489 if (conversation.getMode() == Conversation.MODE_MULTI) {
3490 if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
3491 final Bookmark bookmark = conversation.getBookmark();
3492 if (maySynchronizeWithBookmarks && bookmark != null) {
3493 if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) {
3494 Account account = bookmark.getAccount();
3495 bookmark.setConversation(null);
3496 deleteBookmark(account, bookmark);
3497 } else if (bookmark.autojoin()) {
3498 bookmark.setAutojoin(false);
3499 createBookmark(bookmark.getAccount(), bookmark);
3500 }
3501 }
3502 }
3503 deregisterWithMuc(conversation);
3504 leaveMuc(conversation);
3505 } else {
3506 if (conversation
3507 .getContact()
3508 .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
3509 stopPresenceUpdatesTo(conversation.getContact());
3510 }
3511 }
3512 updateConversation(conversation);
3513 this.conversations.remove(conversation);
3514 updateConversationUi();
3515 }
3516 }
3517
3518 public void stopPresenceUpdatesTo(Contact contact) {
3519 Log.d(Config.LOGTAG, "Canceling presence request from " + contact.getJid().toString());
3520 sendPresencePacket(contact.getAccount(), mPresenceGenerator.stopPresenceUpdatesTo(contact));
3521 contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
3522 }
3523
3524 public void createAccount(final Account account) {
3525 account.initAccountServices(this);
3526 databaseBackend.createAccount(account);
3527 if (CallIntegration.hasSystemFeature(this)) {
3528 CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
3529 }
3530 this.accounts.add(account);
3531 this.reconnectAccountInBackground(account);
3532 updateAccountUi();
3533 syncEnabledAccountSetting();
3534 toggleForegroundService();
3535 }
3536
3537 private void syncEnabledAccountSetting() {
3538 final boolean hasEnabledAccounts = hasEnabledAccounts();
3539 getPreferences()
3540 .edit()
3541 .putBoolean(SystemEventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts)
3542 .apply();
3543 toggleSetProfilePictureActivity(hasEnabledAccounts);
3544 }
3545
3546 private void toggleSetProfilePictureActivity(final boolean enabled) {
3547 try {
3548 final ComponentName name =
3549 new ComponentName(this, ChooseAccountForProfilePictureActivity.class);
3550 final int targetState =
3551 enabled
3552 ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED
3553 : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
3554 getPackageManager()
3555 .setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP);
3556 } catch (IllegalStateException e) {
3557 Log.d(Config.LOGTAG, "unable to toggle profile picture activity");
3558 }
3559 }
3560
3561 public boolean reconfigurePushDistributor() {
3562 return this.unifiedPushBroker.reconfigurePushDistributor();
3563 }
3564
3565 private Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints(
3566 final UnifiedPushBroker.PushTargetMessenger pushTargetMessenger) {
3567 return this.unifiedPushBroker.renewUnifiedPushEndpoints(pushTargetMessenger);
3568 }
3569
3570 public Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints() {
3571 return this.unifiedPushBroker.renewUnifiedPushEndpoints(null);
3572 }
3573
3574 public UnifiedPushBroker getUnifiedPushBroker() {
3575 return this.unifiedPushBroker;
3576 }
3577
3578 private void provisionAccount(final String address, final String password) {
3579 final Jid jid = Jid.of(address);
3580 final Account account = new Account(jid, password);
3581 account.setOption(Account.OPTION_DISABLED, true);
3582 Log.d(Config.LOGTAG, jid.asBareJid().toString() + ": provisioning account");
3583 createAccount(account);
3584 }
3585
3586 public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
3587 new Thread(
3588 () -> {
3589 try {
3590 final X509Certificate[] chain =
3591 KeyChain.getCertificateChain(this, alias);
3592 final X509Certificate cert =
3593 chain != null && chain.length > 0 ? chain[0] : null;
3594 if (cert == null) {
3595 callback.informUser(R.string.unable_to_parse_certificate);
3596 return;
3597 }
3598 Pair<Jid, String> info = CryptoHelper.extractJidAndName(cert);
3599 if (info == null) {
3600 callback.informUser(R.string.certificate_does_not_contain_jid);
3601 return;
3602 }
3603 if (findAccountByJid(info.first) == null) {
3604 final Account account = new Account(info.first, "");
3605 account.setPrivateKeyAlias(alias);
3606 account.setOption(Account.OPTION_DISABLED, true);
3607 account.setOption(Account.OPTION_FIXED_USERNAME, true);
3608 account.setDisplayName(info.second);
3609 createAccount(account);
3610 callback.onAccountCreated(account);
3611 if (Config.X509_VERIFICATION) {
3612 try {
3613 getMemorizingTrustManager()
3614 .getNonInteractive(account.getServer(), null, 0, null)
3615 .checkClientTrusted(chain, "RSA");
3616 } catch (CertificateException e) {
3617 callback.informUser(
3618 R.string.certificate_chain_is_not_trusted);
3619 }
3620 }
3621 } else {
3622 callback.informUser(R.string.account_already_exists);
3623 }
3624 } catch (Exception e) {
3625 callback.informUser(R.string.unable_to_parse_certificate);
3626 }
3627 })
3628 .start();
3629 }
3630
3631 public void updateKeyInAccount(final Account account, final String alias) {
3632 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": update key in account " + alias);
3633 try {
3634 X509Certificate[] chain =
3635 KeyChain.getCertificateChain(XmppConnectionService.this, alias);
3636 Log.d(Config.LOGTAG, account.getJid().asBareJid() + " loaded certificate chain");
3637 Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
3638 if (info == null) {
3639 showErrorToastInUi(R.string.certificate_does_not_contain_jid);
3640 return;
3641 }
3642 if (account.getJid().asBareJid().equals(info.first)) {
3643 account.setPrivateKeyAlias(alias);
3644 account.setDisplayName(info.second);
3645 databaseBackend.updateAccount(account);
3646 if (Config.X509_VERIFICATION) {
3647 try {
3648 getMemorizingTrustManager()
3649 .getNonInteractive()
3650 .checkClientTrusted(chain, "RSA");
3651 } catch (CertificateException e) {
3652 showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
3653 }
3654 account.getAxolotlService().regenerateKeys(true);
3655 }
3656 } else {
3657 showErrorToastInUi(R.string.jid_does_not_match_certificate);
3658 }
3659 } catch (Exception e) {
3660 e.printStackTrace();
3661 }
3662 }
3663
3664 public boolean updateAccount(final Account account) {
3665 if (databaseBackend.updateAccount(account)) {
3666 Integer color = account.getColorToSave();
3667 if (color == null) {
3668 getPreferences().edit().remove("account_color:" + account.getUuid()).commit();
3669 } else {
3670 getPreferences().edit().putInt("account_color:" + account.getUuid(), color.intValue()).commit();
3671 }
3672 account.setShowErrorNotification(true);
3673 this.statusListener.onStatusChanged(account);
3674 databaseBackend.updateAccount(account);
3675 reconnectAccountInBackground(account);
3676 updateAccountUi();
3677 getNotificationService().updateErrorNotification();
3678 toggleForegroundService();
3679 syncEnabledAccountSetting();
3680 mChannelDiscoveryService.cleanCache();
3681 if (CallIntegration.hasSystemFeature(this)) {
3682 CallIntegrationConnectionService.togglePhoneAccountAsync(this, account);
3683 }
3684 return true;
3685 } else {
3686 return false;
3687 }
3688 }
3689
3690 public void updateAccountPasswordOnServer(
3691 final Account account,
3692 final String newPassword,
3693 final OnAccountPasswordChanged callback) {
3694 final Iq iq = getIqGenerator().generateSetPassword(account, newPassword);
3695 sendIqPacket(
3696 account,
3697 iq,
3698 (packet) -> {
3699 if (packet.getType() == Iq.Type.RESULT) {
3700 account.setPassword(newPassword);
3701 account.setOption(Account.OPTION_MAGIC_CREATE, false);
3702 databaseBackend.updateAccount(account);
3703 callback.onPasswordChangeSucceeded();
3704 } else {
3705 callback.onPasswordChangeFailed();
3706 }
3707 });
3708 }
3709
3710 public void unregisterAccount(final Account account, final Consumer<Boolean> callback) {
3711 final Iq iqPacket = new Iq(Iq.Type.SET);
3712 final Element query = iqPacket.addChild("query", Namespace.REGISTER);
3713 query.addChild("remove");
3714 sendIqPacket(
3715 account,
3716 iqPacket,
3717 (response) -> {
3718 if (response.getType() == Iq.Type.RESULT) {
3719 deleteAccount(account);
3720 callback.accept(true);
3721 } else {
3722 callback.accept(false);
3723 }
3724 });
3725 }
3726
3727 public void deleteAccount(final Account account) {
3728 getPreferences().edit().remove("onboarding_continued").commit();
3729 final boolean connected = account.getStatus() == Account.State.ONLINE;
3730 synchronized (this.conversations) {
3731 if (connected) {
3732 account.getAxolotlService().deleteOmemoIdentity();
3733 }
3734 for (final Conversation conversation : conversations) {
3735 if (conversation.getAccount() == account) {
3736 if (conversation.getMode() == Conversation.MODE_MULTI) {
3737 if (connected) {
3738 leaveMuc(conversation);
3739 }
3740 }
3741 conversations.remove(conversation);
3742 mNotificationService.clear(conversation);
3743 }
3744 }
3745 new Thread(() -> {
3746 for (final Contact contact : account.getRoster().getContacts()) {
3747 contact.unregisterAsPhoneAccount(this);
3748 }
3749 }).start();
3750 if (account.getXmppConnection() != null) {
3751 new Thread(() -> disconnect(account, !connected)).start();
3752 }
3753 final Runnable runnable =
3754 () -> {
3755 if (!databaseBackend.deleteAccount(account)) {
3756 Log.d(
3757 Config.LOGTAG,
3758 account.getJid().asBareJid() + ": unable to delete account");
3759 }
3760 };
3761 mDatabaseWriterExecutor.execute(runnable);
3762 this.accounts.remove(account);
3763 if (CallIntegration.hasSystemFeature(this)) {
3764 CallIntegrationConnectionService.unregisterPhoneAccount(this, account);
3765 }
3766 this.mRosterSyncTaskManager.clear(account);
3767 updateAccountUi();
3768 mNotificationService.updateErrorNotification();
3769 syncEnabledAccountSetting();
3770 toggleForegroundService();
3771 }
3772 }
3773
3774 public void setOnConversationListChangedListener(OnConversationUpdate listener) {
3775 final boolean remainingListeners;
3776 synchronized (LISTENER_LOCK) {
3777 remainingListeners = checkListeners();
3778 if (!this.mOnConversationUpdates.add(listener)) {
3779 Log.w(
3780 Config.LOGTAG,
3781 listener.getClass().getName()
3782 + " is already registered as ConversationListChangedListener");
3783 }
3784 this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
3785 }
3786 if (remainingListeners) {
3787 switchToForeground();
3788 }
3789 }
3790
3791 public void removeOnConversationListChangedListener(OnConversationUpdate listener) {
3792 final boolean remainingListeners;
3793 synchronized (LISTENER_LOCK) {
3794 this.mOnConversationUpdates.remove(listener);
3795 this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
3796 remainingListeners = checkListeners();
3797 }
3798 if (remainingListeners) {
3799 switchToBackground();
3800 }
3801 }
3802
3803 public void setOnShowErrorToastListener(OnShowErrorToast listener) {
3804 final boolean remainingListeners;
3805 synchronized (LISTENER_LOCK) {
3806 remainingListeners = checkListeners();
3807 if (!this.mOnShowErrorToasts.add(listener)) {
3808 Log.w(
3809 Config.LOGTAG,
3810 listener.getClass().getName()
3811 + " is already registered as OnShowErrorToastListener");
3812 }
3813 }
3814 if (remainingListeners) {
3815 switchToForeground();
3816 }
3817 }
3818
3819 public void removeOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
3820 final boolean remainingListeners;
3821 synchronized (LISTENER_LOCK) {
3822 this.mOnShowErrorToasts.remove(onShowErrorToast);
3823 remainingListeners = checkListeners();
3824 }
3825 if (remainingListeners) {
3826 switchToBackground();
3827 }
3828 }
3829
3830 public void setOnAccountListChangedListener(OnAccountUpdate listener) {
3831 final boolean remainingListeners;
3832 synchronized (LISTENER_LOCK) {
3833 remainingListeners = checkListeners();
3834 if (!this.mOnAccountUpdates.add(listener)) {
3835 Log.w(
3836 Config.LOGTAG,
3837 listener.getClass().getName()
3838 + " is already registered as OnAccountListChangedtListener");
3839 }
3840 }
3841 if (remainingListeners) {
3842 switchToForeground();
3843 }
3844 }
3845
3846 public void removeOnAccountListChangedListener(OnAccountUpdate listener) {
3847 final boolean remainingListeners;
3848 synchronized (LISTENER_LOCK) {
3849 this.mOnAccountUpdates.remove(listener);
3850 remainingListeners = checkListeners();
3851 }
3852 if (remainingListeners) {
3853 switchToBackground();
3854 }
3855 }
3856
3857 public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
3858 final boolean remainingListeners;
3859 synchronized (LISTENER_LOCK) {
3860 remainingListeners = checkListeners();
3861 if (!this.mOnCaptchaRequested.add(listener)) {
3862 Log.w(
3863 Config.LOGTAG,
3864 listener.getClass().getName()
3865 + " is already registered as OnCaptchaRequestListener");
3866 }
3867 }
3868 if (remainingListeners) {
3869 switchToForeground();
3870 }
3871 }
3872
3873 public void removeOnCaptchaRequestedListener(OnCaptchaRequested listener) {
3874 final boolean remainingListeners;
3875 synchronized (LISTENER_LOCK) {
3876 this.mOnCaptchaRequested.remove(listener);
3877 remainingListeners = checkListeners();
3878 }
3879 if (remainingListeners) {
3880 switchToBackground();
3881 }
3882 }
3883
3884 public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
3885 final boolean remainingListeners;
3886 synchronized (LISTENER_LOCK) {
3887 remainingListeners = checkListeners();
3888 if (!this.mOnRosterUpdates.add(listener)) {
3889 Log.w(
3890 Config.LOGTAG,
3891 listener.getClass().getName()
3892 + " is already registered as OnRosterUpdateListener");
3893 }
3894 }
3895 if (remainingListeners) {
3896 switchToForeground();
3897 }
3898 }
3899
3900 public void removeOnRosterUpdateListener(final OnRosterUpdate listener) {
3901 final boolean remainingListeners;
3902 synchronized (LISTENER_LOCK) {
3903 this.mOnRosterUpdates.remove(listener);
3904 remainingListeners = checkListeners();
3905 }
3906 if (remainingListeners) {
3907 switchToBackground();
3908 }
3909 }
3910
3911 public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
3912 final boolean remainingListeners;
3913 synchronized (LISTENER_LOCK) {
3914 remainingListeners = checkListeners();
3915 if (!this.mOnUpdateBlocklist.add(listener)) {
3916 Log.w(
3917 Config.LOGTAG,
3918 listener.getClass().getName()
3919 + " is already registered as OnUpdateBlocklistListener");
3920 }
3921 }
3922 if (remainingListeners) {
3923 switchToForeground();
3924 }
3925 }
3926
3927 public void removeOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
3928 final boolean remainingListeners;
3929 synchronized (LISTENER_LOCK) {
3930 this.mOnUpdateBlocklist.remove(listener);
3931 remainingListeners = checkListeners();
3932 }
3933 if (remainingListeners) {
3934 switchToBackground();
3935 }
3936 }
3937
3938 public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
3939 final boolean remainingListeners;
3940 synchronized (LISTENER_LOCK) {
3941 remainingListeners = checkListeners();
3942 if (!this.mOnKeyStatusUpdated.add(listener)) {
3943 Log.w(
3944 Config.LOGTAG,
3945 listener.getClass().getName()
3946 + " is already registered as OnKeyStatusUpdateListener");
3947 }
3948 }
3949 if (remainingListeners) {
3950 switchToForeground();
3951 }
3952 }
3953
3954 public void removeOnNewKeysAvailableListener(final OnKeyStatusUpdated listener) {
3955 final boolean remainingListeners;
3956 synchronized (LISTENER_LOCK) {
3957 this.mOnKeyStatusUpdated.remove(listener);
3958 remainingListeners = checkListeners();
3959 }
3960 if (remainingListeners) {
3961 switchToBackground();
3962 }
3963 }
3964
3965 public void setOnRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
3966 final boolean remainingListeners;
3967 synchronized (LISTENER_LOCK) {
3968 remainingListeners = checkListeners();
3969 if (!this.onJingleRtpConnectionUpdate.add(listener)) {
3970 Log.w(
3971 Config.LOGTAG,
3972 listener.getClass().getName()
3973 + " is already registered as OnJingleRtpConnectionUpdate");
3974 }
3975 }
3976 if (remainingListeners) {
3977 switchToForeground();
3978 }
3979 }
3980
3981 public void removeRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
3982 final boolean remainingListeners;
3983 synchronized (LISTENER_LOCK) {
3984 this.onJingleRtpConnectionUpdate.remove(listener);
3985 remainingListeners = checkListeners();
3986 }
3987 if (remainingListeners) {
3988 switchToBackground();
3989 }
3990 }
3991
3992 public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
3993 final boolean remainingListeners;
3994 synchronized (LISTENER_LOCK) {
3995 remainingListeners = checkListeners();
3996 if (!this.mOnMucRosterUpdate.add(listener)) {
3997 Log.w(
3998 Config.LOGTAG,
3999 listener.getClass().getName()
4000 + " is already registered as OnMucRosterListener");
4001 }
4002 }
4003 if (remainingListeners) {
4004 switchToForeground();
4005 }
4006 }
4007
4008 public void removeOnMucRosterUpdateListener(final OnMucRosterUpdate listener) {
4009 final boolean remainingListeners;
4010 synchronized (LISTENER_LOCK) {
4011 this.mOnMucRosterUpdate.remove(listener);
4012 remainingListeners = checkListeners();
4013 }
4014 if (remainingListeners) {
4015 switchToBackground();
4016 }
4017 }
4018
4019 public boolean checkListeners() {
4020 return (this.mOnAccountUpdates.isEmpty()
4021 && this.mOnConversationUpdates.isEmpty()
4022 && this.mOnRosterUpdates.isEmpty()
4023 && this.mOnCaptchaRequested.isEmpty()
4024 && this.mOnMucRosterUpdate.isEmpty()
4025 && this.mOnUpdateBlocklist.isEmpty()
4026 && this.mOnShowErrorToasts.isEmpty()
4027 && this.onJingleRtpConnectionUpdate.isEmpty()
4028 && this.mOnKeyStatusUpdated.isEmpty());
4029 }
4030
4031 private void switchToForeground() {
4032 toggleSoftDisabled(false);
4033 final boolean broadcastLastActivity = broadcastLastActivity();
4034 for (Conversation conversation : getConversations()) {
4035 if (conversation.getMode() == Conversation.MODE_MULTI) {
4036 conversation.getMucOptions().resetChatState();
4037 } else {
4038 conversation.setIncomingChatState(Config.DEFAULT_CHAT_STATE);
4039 }
4040 }
4041 for (Account account : getAccounts()) {
4042 if (account.getStatus() == Account.State.ONLINE) {
4043 account.deactivateGracePeriod();
4044 final XmppConnection connection = account.getXmppConnection();
4045 if (connection != null) {
4046 if (connection.getFeatures().csi()) {
4047 connection.sendActive();
4048 }
4049 if (broadcastLastActivity) {
4050 sendPresence(
4051 account,
4052 false); // send new presence but don't include idle because we are
4053 // not
4054 }
4055 }
4056 }
4057 }
4058 Log.d(Config.LOGTAG, "app switched into foreground");
4059 }
4060
4061 private void switchToBackground() {
4062 final boolean broadcastLastActivity = broadcastLastActivity();
4063 if (broadcastLastActivity) {
4064 mLastActivity = System.currentTimeMillis();
4065 final SharedPreferences.Editor editor = getPreferences().edit();
4066 editor.putLong(SETTING_LAST_ACTIVITY_TS, mLastActivity);
4067 editor.apply();
4068 }
4069 for (Account account : getAccounts()) {
4070 if (account.getStatus() == Account.State.ONLINE) {
4071 XmppConnection connection = account.getXmppConnection();
4072 if (connection != null) {
4073 if (broadcastLastActivity) {
4074 sendPresence(account, true);
4075 }
4076 if (connection.getFeatures().csi()) {
4077 connection.sendInactive();
4078 }
4079 }
4080 }
4081 }
4082 this.mNotificationService.setIsInForeground(false);
4083 Log.d(Config.LOGTAG, "app switched into background");
4084 }
4085
4086 public void connectMultiModeConversations(Account account) {
4087 List<Conversation> conversations = getConversations();
4088 for (Conversation conversation : conversations) {
4089 if (conversation.getMode() == Conversation.MODE_MULTI
4090 && conversation.getAccount() == account) {
4091 joinMuc(conversation);
4092 }
4093 }
4094 }
4095
4096 public void mucSelfPingAndRejoin(final Conversation conversation) {
4097 final Account account = conversation.getAccount();
4098 synchronized (account.inProgressConferenceJoins) {
4099 if (account.inProgressConferenceJoins.contains(conversation)) {
4100 Log.d(
4101 Config.LOGTAG,
4102 account.getJid().asBareJid()
4103 + ": canceling muc self ping because join is already under way");
4104 return;
4105 }
4106 }
4107 synchronized (account.inProgressConferencePings) {
4108 if (!account.inProgressConferencePings.add(conversation)) {
4109 Log.d(
4110 Config.LOGTAG,
4111 account.getJid().asBareJid()
4112 + ": canceling muc self ping because ping is already under way");
4113 return;
4114 }
4115 }
4116 final Jid self = conversation.getMucOptions().getSelf().getFullJid();
4117 final Iq ping = new Iq(Iq.Type.GET);
4118 ping.setTo(self);
4119 ping.addChild("ping", Namespace.PING);
4120 sendIqPacket(
4121 conversation.getAccount(),
4122 ping,
4123 (response) -> {
4124 if (response.getType() == Iq.Type.ERROR) {
4125 final var error = response.getError();
4126 if (error == null
4127 || error.hasChild("service-unavailable")
4128 || error.hasChild("feature-not-implemented")
4129 || error.hasChild("item-not-found")) {
4130 Log.d(
4131 Config.LOGTAG,
4132 account.getJid().asBareJid()
4133 + ": ping to "
4134 + self
4135 + " came back as ignorable error");
4136 } else {
4137 Log.d(
4138 Config.LOGTAG,
4139 account.getJid().asBareJid()
4140 + ": ping to "
4141 + self
4142 + " failed. attempting rejoin");
4143 joinMuc(conversation);
4144 }
4145 } else if (response.getType() == Iq.Type.RESULT) {
4146 Log.d(
4147 Config.LOGTAG,
4148 account.getJid().asBareJid()
4149 + ": ping to "
4150 + self
4151 + " came back fine");
4152 }
4153 synchronized (account.inProgressConferencePings) {
4154 account.inProgressConferencePings.remove(conversation);
4155 }
4156 });
4157 }
4158
4159 public void joinMuc(Conversation conversation) {
4160 joinMuc(conversation, null, false);
4161 }
4162
4163 public void joinMuc(Conversation conversation, boolean followedInvite) {
4164 joinMuc(conversation, null, followedInvite);
4165 }
4166
4167 private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
4168 joinMuc(conversation, onConferenceJoined, false);
4169 }
4170
4171 private void joinMuc(
4172 Conversation conversation,
4173 final OnConferenceJoined onConferenceJoined,
4174 final boolean followedInvite) {
4175 final Account account = conversation.getAccount();
4176 synchronized (account.pendingConferenceJoins) {
4177 account.pendingConferenceJoins.remove(conversation);
4178 }
4179 synchronized (account.pendingConferenceLeaves) {
4180 account.pendingConferenceLeaves.remove(conversation);
4181 }
4182 if (account.getStatus() == Account.State.ONLINE) {
4183 synchronized (account.inProgressConferenceJoins) {
4184 account.inProgressConferenceJoins.add(conversation);
4185 }
4186 if (Config.MUC_LEAVE_BEFORE_JOIN) {
4187 sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions()));
4188 }
4189 conversation.resetMucOptions();
4190 if (onConferenceJoined != null) {
4191 conversation.getMucOptions().flagNoAutoPushConfiguration();
4192 }
4193 conversation.setHasMessagesLeftOnServer(false);
4194 fetchConferenceConfiguration(
4195 conversation,
4196 new OnConferenceConfigurationFetched() {
4197
4198 private void join(Conversation conversation) {
4199 Account account = conversation.getAccount();
4200 final MucOptions mucOptions = conversation.getMucOptions();
4201
4202 if (mucOptions.nonanonymous()
4203 && !mucOptions.membersOnly()
4204 && !conversation.getBooleanAttribute(
4205 "accept_non_anonymous", false)) {
4206 synchronized (account.inProgressConferenceJoins) {
4207 account.inProgressConferenceJoins.remove(conversation);
4208 }
4209 mucOptions.setError(MucOptions.Error.NON_ANONYMOUS);
4210 updateConversationUi();
4211 if (onConferenceJoined != null) {
4212 onConferenceJoined.onConferenceJoined(conversation);
4213 }
4214 return;
4215 }
4216
4217 final Jid joinJid = mucOptions.getSelf().getFullJid();
4218 Log.d(
4219 Config.LOGTAG,
4220 account.getJid().asBareJid().toString()
4221 + ": joining conversation "
4222 + joinJid.toString());
4223 final var packet =
4224 mPresenceGenerator.selfPresence(
4225 account,
4226 Presence.Status.ONLINE,
4227 mucOptions.nonanonymous()
4228 || onConferenceJoined != null,
4229 mucOptions.getSelf().getNick());
4230 packet.setTo(joinJid);
4231 Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
4232 if (conversation.getMucOptions().getPassword() != null) {
4233 x.addChild("password").setContent(mucOptions.getPassword());
4234 }
4235
4236 if (mucOptions.mamSupport()) {
4237 // Use MAM instead of the limited muc history to get history
4238 x.addChild("history").setAttribute("maxchars", "0");
4239 } else {
4240 // Fallback to muc history
4241 x.addChild("history")
4242 .setAttribute(
4243 "since",
4244 PresenceGenerator.getTimestamp(
4245 conversation
4246 .getLastMessageTransmitted()
4247 .getTimestamp()));
4248 }
4249 sendPresencePacket(account, packet);
4250 if (onConferenceJoined != null) {
4251 onConferenceJoined.onConferenceJoined(conversation);
4252 }
4253 if (!joinJid.equals(conversation.getJid())) {
4254 conversation.setContactJid(joinJid);
4255 databaseBackend.updateConversation(conversation);
4256 }
4257
4258 maybeRegisterWithMuc(conversation, null);
4259
4260 if (mucOptions.mamSupport()) {
4261 getMessageArchiveService().catchupMUC(conversation);
4262 }
4263 fetchConferenceMembers(conversation);
4264 if (mucOptions.isPrivateAndNonAnonymous()) {
4265 if (followedInvite) {
4266 final Bookmark bookmark = conversation.getBookmark();
4267 if (bookmark != null) {
4268 if (!bookmark.autojoin()) {
4269 bookmark.setAutojoin(true);
4270 createBookmark(account, bookmark);
4271 }
4272 } else {
4273 saveConversationAsBookmark(conversation, null);
4274 }
4275 }
4276 }
4277 synchronized (account.inProgressConferenceJoins) {
4278 account.inProgressConferenceJoins.remove(conversation);
4279 sendUnsentMessages(conversation);
4280 }
4281 }
4282
4283 @Override
4284 public void onConferenceConfigurationFetched(Conversation conversation) {
4285 if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
4286 Log.d(
4287 Config.LOGTAG,
4288 account.getJid().asBareJid()
4289 + ": conversation ("
4290 + conversation.getJid()
4291 + ") got archived before IQ result");
4292 return;
4293 }
4294 join(conversation);
4295 }
4296
4297 @Override
4298 public void onFetchFailed(
4299 final Conversation conversation, final String errorCondition) {
4300 if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
4301 Log.d(
4302 Config.LOGTAG,
4303 account.getJid().asBareJid()
4304 + ": conversation ("
4305 + conversation.getJid()
4306 + ") got archived before IQ result");
4307 return;
4308 }
4309 if ("remote-server-not-found".equals(errorCondition)) {
4310 synchronized (account.inProgressConferenceJoins) {
4311 account.inProgressConferenceJoins.remove(conversation);
4312 }
4313 conversation
4314 .getMucOptions()
4315 .setError(MucOptions.Error.SERVER_NOT_FOUND);
4316 updateConversationUi();
4317 } else {
4318 join(conversation);
4319 fetchConferenceConfiguration(conversation);
4320 }
4321 }
4322 });
4323 updateConversationUi();
4324 } else {
4325 synchronized (account.pendingConferenceJoins) {
4326 account.pendingConferenceJoins.add(conversation);
4327 }
4328 conversation.resetMucOptions();
4329 conversation.setHasMessagesLeftOnServer(false);
4330 updateConversationUi();
4331 }
4332 }
4333
4334 private void fetchConferenceMembers(final Conversation conversation) {
4335 final Account account = conversation.getAccount();
4336 final AxolotlService axolotlService = account.getAxolotlService();
4337 final var affiliations = new ArrayList<String>();
4338 affiliations.add("outcast");
4339 if (conversation.getMucOptions().isPrivateAndNonAnonymous()) affiliations.addAll(List.of("member", "admin", "owner"));
4340 final Consumer<Iq> callback =
4341 new Consumer<Iq>() {
4342
4343 private int i = 0;
4344 private boolean success = true;
4345
4346 @Override
4347 public void accept(Iq response) {
4348 final boolean omemoEnabled =
4349 conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL;
4350 Element query = response.query("http://jabber.org/protocol/muc#admin");
4351 if (response.getType() == Iq.Type.RESULT && query != null) {
4352 for (Element child : query.getChildren()) {
4353 if ("item".equals(child.getName())) {
4354 MucOptions.User user =
4355 AbstractParser.parseItem(conversation, child);
4356 user.setOnline(false);
4357 if (!user.realJidMatchesAccount()) {
4358 boolean isNew =
4359 conversation.getMucOptions().updateUser(user);
4360 Contact contact = user.getContact();
4361 if (omemoEnabled
4362 && isNew
4363 && user.getRealJid() != null
4364 && (contact == null
4365 || !contact.mutualPresenceSubscription())
4366 && axolotlService.hasEmptyDeviceList(
4367 user.getRealJid())) {
4368 axolotlService.fetchDeviceIds(user.getRealJid());
4369 }
4370 }
4371 }
4372 }
4373 } else {
4374 success = false;
4375 Log.d(
4376 Config.LOGTAG,
4377 account.getJid().asBareJid()
4378 + ": could not request affiliation "
4379 + affiliations.get(i)
4380 + " in "
4381 + conversation.getJid().asBareJid());
4382 }
4383 ++i;
4384 if (i >= affiliations.size()) {
4385 final var mucOptions = conversation.getMucOptions();
4386 List<Jid> members = mucOptions.getMembers(true);
4387 if (success) {
4388 List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
4389 boolean changed = false;
4390 for (ListIterator<Jid> iterator = cryptoTargets.listIterator();
4391 iterator.hasNext(); ) {
4392 Jid jid = iterator.next();
4393 if (!members.contains(jid)
4394 && !members.contains(jid.getDomain())) {
4395 iterator.remove();
4396 Log.d(
4397 Config.LOGTAG,
4398 account.getJid().asBareJid()
4399 + ": removed "
4400 + jid
4401 + " from crypto targets of "
4402 + conversation.getName());
4403 changed = true;
4404 }
4405 }
4406 if (changed) {
4407 conversation.setAcceptedCryptoTargets(cryptoTargets);
4408 updateConversation(conversation);
4409 }
4410 }
4411 getAvatarService().clear(mucOptions);
4412 updateMucRosterUi();
4413 updateConversationUi();
4414 }
4415 }
4416 };
4417 for (String affiliation : affiliations) {
4418 sendIqPacket(
4419 account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
4420 }
4421 Log.d(
4422 Config.LOGTAG,
4423 account.getJid().asBareJid() + ": fetching members for " + conversation.getName());
4424 }
4425
4426 public void providePasswordForMuc(final Conversation conversation, final String password) {
4427 if (conversation.getMode() == Conversation.MODE_MULTI) {
4428 conversation.getMucOptions().setPassword(password);
4429 if (conversation.getBookmark() != null) {
4430 final Bookmark bookmark = conversation.getBookmark();
4431 bookmark.setAutojoin(true);
4432 createBookmark(conversation.getAccount(), bookmark);
4433 }
4434 updateConversation(conversation);
4435 joinMuc(conversation);
4436 }
4437 }
4438
4439 public void deleteAvatar(final Account account) {
4440 final AtomicBoolean executed = new AtomicBoolean(false);
4441 final Runnable onDeleted =
4442 () -> {
4443 if (executed.compareAndSet(false, true)) {
4444 account.setAvatar(null);
4445 databaseBackend.updateAccount(account);
4446 getAvatarService().clear(account);
4447 updateAccountUi();
4448 }
4449 };
4450 deleteVcardAvatar(account, onDeleted);
4451 deletePepNode(account, Namespace.AVATAR_DATA);
4452 deletePepNode(account, Namespace.AVATAR_METADATA, onDeleted);
4453 }
4454
4455 public void deletePepNode(final Account account, final String node) {
4456 deletePepNode(account, node, null);
4457 }
4458
4459 private void deletePepNode(final Account account, final String node, final Runnable runnable) {
4460 final Iq request = mIqGenerator.deleteNode(node);
4461 sendIqPacket(
4462 account,
4463 request,
4464 (packet) -> {
4465 if (packet.getType() == Iq.Type.RESULT) {
4466 Log.d(
4467 Config.LOGTAG,
4468 account.getJid().asBareJid()
4469 + ": successfully deleted pep node "
4470 + node);
4471 if (runnable != null) {
4472 runnable.run();
4473 }
4474 } else {
4475 Log.d(
4476 Config.LOGTAG,
4477 account.getJid().asBareJid() + ": failed to delete " + packet);
4478 }
4479 });
4480 }
4481
4482 private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) {
4483 final Iq retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid());
4484 sendIqPacket(
4485 account,
4486 retrieveVcard,
4487 (response) -> {
4488 if (response.getType() != Iq.Type.RESULT) {
4489 Log.d(
4490 Config.LOGTAG,
4491 account.getJid().asBareJid() + ": no vCard set. nothing to do");
4492 return;
4493 }
4494 final Element vcard = response.findChild("vCard", "vcard-temp");
4495 if (vcard == null) {
4496 Log.d(
4497 Config.LOGTAG,
4498 account.getJid().asBareJid() + ": no vCard set. nothing to do");
4499 return;
4500 }
4501 Element photo = vcard.findChild("PHOTO");
4502 if (photo == null) {
4503 photo = vcard.addChild("PHOTO");
4504 }
4505 photo.clearChildren();
4506 final Iq publication = new Iq(Iq.Type.SET);
4507 publication.setTo(account.getJid().asBareJid());
4508 publication.addChild(vcard);
4509 sendIqPacket(
4510 account,
4511 publication,
4512 (publicationResponse) -> {
4513 if (publicationResponse.getType() == Iq.Type.RESULT) {
4514 Log.d(
4515 Config.LOGTAG,
4516 account.getJid().asBareJid()
4517 + ": successfully deleted vcard avatar");
4518 runnable.run();
4519 } else {
4520 Log.d(
4521 Config.LOGTAG,
4522 "failed to publish vcard "
4523 + publicationResponse.getErrorCondition());
4524 }
4525 });
4526 });
4527 }
4528
4529 private boolean hasEnabledAccounts() {
4530 if (this.accounts == null) {
4531 return false;
4532 }
4533 for (final Account account : this.accounts) {
4534 if (account.isConnectionEnabled()) {
4535 return true;
4536 }
4537 }
4538 return false;
4539 }
4540
4541 public void getAttachments(
4542 final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) {
4543 getAttachments(
4544 conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded);
4545 }
4546
4547 public void getAttachments(
4548 final Account account,
4549 final Jid jid,
4550 final int limit,
4551 final OnMediaLoaded onMediaLoaded) {
4552 getAttachments(account.getUuid(), jid.asBareJid(), limit, onMediaLoaded);
4553 }
4554
4555 public void getAttachments(
4556 final String account,
4557 final Jid jid,
4558 final int limit,
4559 final OnMediaLoaded onMediaLoaded) {
4560 new Thread(
4561 () ->
4562 onMediaLoaded.onMediaLoaded(
4563 fileBackend.convertToAttachments(
4564 databaseBackend.getRelativeFilePaths(
4565 account, jid, limit))))
4566 .start();
4567 }
4568
4569 public void persistSelfNick(final MucOptions.User self, final boolean modified) {
4570 final Conversation conversation = self.getConversation();
4571 final Account account = conversation.getAccount();
4572 final Jid full = self.getFullJid();
4573 if (!full.equals(conversation.getJid())) {
4574 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persisting full jid " + full);
4575 conversation.setContactJid(full);
4576 databaseBackend.updateConversation(conversation);
4577 }
4578
4579 final String nick = self.getNick();
4580 final Bookmark bookmark = conversation.getBookmark();
4581 if (bookmark == null || !modified) {
4582 return;
4583 }
4584 final String defaultNick = MucOptions.defaultNick(account);
4585 if (nick.equals(defaultNick) || nick.equals(bookmark.getNick())) {
4586 return;
4587 }
4588 Log.d(
4589 Config.LOGTAG,
4590 account.getJid().asBareJid()
4591 + ": persist nick '"
4592 + full.getResource()
4593 + "' into bookmark for "
4594 + conversation.getJid().asBareJid());
4595 bookmark.setNick(nick);
4596 createBookmark(bookmark.getAccount(), bookmark);
4597 }
4598
4599 public void presenceToMuc(final Conversation conversation) {
4600 final MucOptions options = conversation.getMucOptions();
4601 if (options.online()) {
4602 Account account = conversation.getAccount();
4603 final Jid joinJid = options.getSelf().getFullJid();
4604 final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), options.getSelf().getNick());
4605 packet.setTo(joinJid);
4606 sendPresencePacket(account, packet);
4607 }
4608 }
4609
4610 public boolean renameInMuc(
4611 final Conversation conversation,
4612 final String nick,
4613 final UiCallback<Conversation> callback) {
4614 final Account account = conversation.getAccount();
4615 final Bookmark bookmark = conversation.getBookmark();
4616 final MucOptions options = conversation.getMucOptions();
4617 final Jid joinJid = options.createJoinJid(nick);
4618 if (joinJid == null) {
4619 return false;
4620 }
4621 if (options.online()) {
4622 maybeRegisterWithMuc(conversation, nick);
4623 options.setOnRenameListener(
4624 new OnRenameListener() {
4625
4626 @Override
4627 public void onSuccess() {
4628 final var packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous(), nick);
4629 packet.setTo(joinJid);
4630 sendPresencePacket(account, packet);
4631 callback.success(conversation);
4632 }
4633
4634 @Override
4635 public void onFailure() {
4636 callback.error(R.string.nick_in_use, conversation);
4637 }
4638 });
4639
4640 final var packet =
4641 mPresenceGenerator.selfPresence(
4642 account, Presence.Status.ONLINE, options.nonanonymous(), nick);
4643 packet.setTo(joinJid);
4644 sendPresencePacket(account, packet);
4645 if (nick.equals(MucOptions.defaultNick(account))
4646 && bookmark != null
4647 && bookmark.getNick() != null) {
4648 Log.d(
4649 Config.LOGTAG,
4650 account.getJid().asBareJid()
4651 + ": removing nick from bookmark for "
4652 + bookmark.getJid());
4653 bookmark.setNick(null);
4654 createBookmark(account, bookmark);
4655 }
4656 } else {
4657 conversation.setContactJid(joinJid);
4658 databaseBackend.updateConversation(conversation);
4659 if (account.getStatus() == Account.State.ONLINE) {
4660 if (bookmark != null) {
4661 bookmark.setNick(nick);
4662 createBookmark(account, bookmark);
4663 }
4664 joinMuc(conversation);
4665 }
4666 }
4667 return true;
4668 }
4669
4670 public void checkMucRequiresRename() {
4671 synchronized (this.conversations) {
4672 for (final Conversation conversation : this.conversations) {
4673 if (conversation.getMode() == Conversational.MODE_MULTI) {
4674 checkMucRequiresRename(conversation);
4675 }
4676 }
4677 }
4678 }
4679
4680 private void checkMucRequiresRename(final Conversation conversation) {
4681 final var options = conversation.getMucOptions();
4682 if (!options.online()) {
4683 return;
4684 }
4685 final var account = conversation.getAccount();
4686 final String current = options.getActualNick();
4687 final String proposed = options.getProposedNickPure();
4688 if (current == null || current.equals(proposed)) {
4689 return;
4690 }
4691 final Jid joinJid = options.createJoinJid(proposed);
4692 Log.d(
4693 Config.LOGTAG,
4694 String.format(
4695 "%s: muc rename required %s (was: %s)",
4696 account.getJid().asBareJid(), joinJid, current));
4697 final var packet =
4698 mPresenceGenerator.selfPresence(
4699 account, Presence.Status.ONLINE, options.nonanonymous(), proposed);
4700 packet.setTo(joinJid);
4701 sendPresencePacket(account, packet);
4702 }
4703
4704 public void leaveMuc(Conversation conversation) {
4705 leaveMuc(conversation, false);
4706 }
4707
4708 private void leaveMuc(Conversation conversation, boolean now) {
4709 final Account account = conversation.getAccount();
4710 synchronized (account.pendingConferenceJoins) {
4711 account.pendingConferenceJoins.remove(conversation);
4712 }
4713 synchronized (account.pendingConferenceLeaves) {
4714 account.pendingConferenceLeaves.remove(conversation);
4715 }
4716 if (account.getStatus() == Account.State.ONLINE || now) {
4717 sendPresencePacket(
4718 conversation.getAccount(),
4719 mPresenceGenerator.leave(conversation.getMucOptions()));
4720 conversation.getMucOptions().setOffline();
4721 Bookmark bookmark = conversation.getBookmark();
4722 if (bookmark != null) {
4723 bookmark.setConversation(null);
4724 }
4725 Log.d(
4726 Config.LOGTAG,
4727 conversation.getAccount().getJid().asBareJid()
4728 + ": leaving muc "
4729 + conversation.getJid());
4730 } else {
4731 synchronized (account.pendingConferenceLeaves) {
4732 account.pendingConferenceLeaves.add(conversation);
4733 }
4734 }
4735 }
4736
4737 public String findConferenceServer(final Account account) {
4738 String server;
4739 if (account.getXmppConnection() != null) {
4740 server = account.getXmppConnection().getMucServer();
4741 if (server != null) {
4742 return server;
4743 }
4744 }
4745 for (Account other : getAccounts()) {
4746 if (other != account && other.getXmppConnection() != null) {
4747 server = other.getXmppConnection().getMucServer();
4748 if (server != null) {
4749 return server;
4750 }
4751 }
4752 }
4753 return null;
4754 }
4755
4756 public void createPublicChannel(
4757 final Account account,
4758 final String name,
4759 final Jid address,
4760 final UiCallback<Conversation> callback) {
4761 joinMuc(
4762 findOrCreateConversation(account, address, true, false, true),
4763 conversation -> {
4764 final Bundle configuration = IqGenerator.defaultChannelConfiguration();
4765 if (!TextUtils.isEmpty(name)) {
4766 configuration.putString("muc#roomconfig_roomname", name);
4767 }
4768 pushConferenceConfiguration(
4769 conversation,
4770 configuration,
4771 new OnConfigurationPushed() {
4772 @Override
4773 public void onPushSucceeded() {
4774 saveConversationAsBookmark(conversation, name);
4775 callback.success(conversation);
4776 }
4777
4778 @Override
4779 public void onPushFailed() {
4780 if (conversation
4781 .getMucOptions()
4782 .getSelf()
4783 .getAffiliation()
4784 .ranks(MucOptions.Affiliation.OWNER)) {
4785 callback.error(
4786 R.string.unable_to_set_channel_configuration,
4787 conversation);
4788 } else {
4789 callback.error(
4790 R.string.joined_an_existing_channel, conversation);
4791 }
4792 }
4793 });
4794 });
4795 }
4796
4797 public boolean createAdhocConference(
4798 final Account account,
4799 final String name,
4800 final Iterable<Jid> jids,
4801 final UiCallback<Conversation> callback) {
4802 Log.d(
4803 Config.LOGTAG,
4804 account.getJid().asBareJid().toString()
4805 + ": creating adhoc conference with "
4806 + jids.toString());
4807 if (account.getStatus() == Account.State.ONLINE) {
4808 try {
4809 String server = findConferenceServer(account);
4810 if (server == null) {
4811 if (callback != null) {
4812 callback.error(R.string.no_conference_server_found, null);
4813 }
4814 return false;
4815 }
4816 final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null);
4817 final Conversation conversation =
4818 findOrCreateConversation(account, jid, true, false, true);
4819 joinMuc(
4820 conversation,
4821 new OnConferenceJoined() {
4822 @Override
4823 public void onConferenceJoined(final Conversation conversation) {
4824 final Bundle configuration =
4825 IqGenerator.defaultGroupChatConfiguration();
4826 if (!TextUtils.isEmpty(name)) {
4827 configuration.putString("muc#roomconfig_roomname", name);
4828 }
4829 pushConferenceConfiguration(
4830 conversation,
4831 configuration,
4832 new OnConfigurationPushed() {
4833 @Override
4834 public void onPushSucceeded() {
4835 for (Jid invite : jids) {
4836 invite(conversation, invite);
4837 }
4838 for (String resource :
4839 account.getSelfContact()
4840 .getPresences()
4841 .toResourceArray()) {
4842 Jid other =
4843 account.getJid().withResource(resource);
4844 Log.d(
4845 Config.LOGTAG,
4846 account.getJid().asBareJid()
4847 + ": sending direct invite to "
4848 + other);
4849 directInvite(conversation, other);
4850 }
4851 saveConversationAsBookmark(conversation, name);
4852 if (callback != null) {
4853 callback.success(conversation);
4854 }
4855 }
4856
4857 @Override
4858 public void onPushFailed() {
4859 archiveConversation(conversation);
4860 if (callback != null) {
4861 callback.error(
4862 R.string.conference_creation_failed,
4863 conversation);
4864 }
4865 }
4866 });
4867 }
4868 });
4869 return true;
4870 } catch (IllegalArgumentException e) {
4871 if (callback != null) {
4872 callback.error(R.string.conference_creation_failed, null);
4873 }
4874 return false;
4875 }
4876 } else {
4877 if (callback != null) {
4878 callback.error(R.string.not_connected_try_again, null);
4879 }
4880 return false;
4881 }
4882 }
4883
4884 public void checkIfMuc(final Account account, final Jid jid, Consumer<Boolean> cb) {
4885 if (jid.isDomainJid()) {
4886 // Spec basically says MUC needs to have a node
4887 // And also specifies that MUC and MUC service should have the same identity...
4888 cb.accept(false);
4889 return;
4890 }
4891
4892 final var request = mIqGenerator.queryDiscoInfo(jid.asBareJid());
4893 sendIqPacket(account, request, (reply) -> {
4894 final var result = new ServiceDiscoveryResult(reply);
4895 cb.accept(
4896 result.getFeatures().contains("http://jabber.org/protocol/muc") &&
4897 result.hasIdentity("conference", null)
4898 );
4899 });
4900 }
4901
4902 public void fetchConferenceConfiguration(final Conversation conversation) {
4903 fetchConferenceConfiguration(conversation, null);
4904 }
4905
4906 public void fetchConferenceConfiguration(
4907 final Conversation conversation, final OnConferenceConfigurationFetched callback) {
4908 final Iq request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid());
4909 final var account = conversation.getAccount();
4910 sendIqPacket(
4911 account,
4912 request,
4913 response -> {
4914 if (response.getType() == Iq.Type.RESULT) {
4915 final MucOptions mucOptions = conversation.getMucOptions();
4916 final Bookmark bookmark = conversation.getBookmark();
4917 final boolean sameBefore =
4918 StringUtils.equals(
4919 bookmark == null ? null : bookmark.getBookmarkName(),
4920 mucOptions.getName());
4921
4922 if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(response))) {
4923 Log.d(
4924 Config.LOGTAG,
4925 account.getJid().asBareJid()
4926 + ": muc configuration changed for "
4927 + conversation.getJid().asBareJid());
4928 updateConversation(conversation);
4929 }
4930
4931 if (bookmark != null
4932 && (sameBefore || bookmark.getBookmarkName() == null)) {
4933 if (bookmark.setBookmarkName(
4934 StringUtils.nullOnEmpty(mucOptions.getName()))) {
4935 createBookmark(account, bookmark);
4936 }
4937 }
4938
4939 if (callback != null) {
4940 callback.onConferenceConfigurationFetched(conversation);
4941 }
4942
4943 updateConversationUi();
4944 } else if (response.getType() == Iq.Type.TIMEOUT) {
4945 Log.d(
4946 Config.LOGTAG,
4947 account.getJid().asBareJid()
4948 + ": received timeout waiting for conference configuration"
4949 + " fetch");
4950 } else {
4951 if (callback != null) {
4952 callback.onFetchFailed(conversation, response.getErrorCondition());
4953 }
4954 }
4955 });
4956 }
4957
4958 public void pushNodeConfiguration(
4959 Account account,
4960 final String node,
4961 final Bundle options,
4962 final OnConfigurationPushed callback) {
4963 pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback);
4964 }
4965
4966 public void pushNodeConfiguration(
4967 Account account,
4968 final Jid jid,
4969 final String node,
4970 final Bundle options,
4971 final OnConfigurationPushed callback) {
4972 Log.d(Config.LOGTAG, "pushing node configuration");
4973 sendIqPacket(
4974 account,
4975 mIqGenerator.requestPubsubConfiguration(jid, node),
4976 responseToRequest -> {
4977 if (responseToRequest.getType() == Iq.Type.RESULT) {
4978 Element pubsub =
4979 responseToRequest.findChild(
4980 "pubsub", "http://jabber.org/protocol/pubsub#owner");
4981 Element configuration =
4982 pubsub == null ? null : pubsub.findChild("configure");
4983 Element x =
4984 configuration == null
4985 ? null
4986 : configuration.findChild("x", Namespace.DATA);
4987 if (x != null) {
4988 final Data data = Data.parse(x);
4989 data.submit(options);
4990 sendIqPacket(
4991 account,
4992 mIqGenerator.publishPubsubConfiguration(jid, node, data),
4993 responseToPublish -> {
4994 if (responseToPublish.getType() == Iq.Type.RESULT
4995 && callback != null) {
4996 Log.d(
4997 Config.LOGTAG,
4998 account.getJid().asBareJid()
4999 + ": successfully changed node"
5000 + " configuration for node "
5001 + node);
5002 callback.onPushSucceeded();
5003 } else if (responseToPublish.getType() == Iq.Type.ERROR
5004 && callback != null) {
5005 callback.onPushFailed();
5006 }
5007 });
5008 } else if (callback != null) {
5009 callback.onPushFailed();
5010 }
5011 } else if (responseToRequest.getType() == Iq.Type.ERROR && callback != null) {
5012 callback.onPushFailed();
5013 }
5014 });
5015 }
5016
5017 public void pushConferenceConfiguration(
5018 final Conversation conversation,
5019 final Bundle options,
5020 final OnConfigurationPushed callback) {
5021 if (options.getString("muc#roomconfig_whois", "moderators").equals("anyone")) {
5022 conversation.setAttribute("accept_non_anonymous", true);
5023 updateConversation(conversation);
5024 }
5025 if (options.containsKey("muc#roomconfig_moderatedroom")) {
5026 final boolean moderated = "1".equals(options.getString("muc#roomconfig_moderatedroom"));
5027 options.putString("members_by_default", moderated ? "0" : "1");
5028 }
5029 if (options.containsKey("muc#roomconfig_allowpm")) {
5030 // ejabberd :-/
5031 final boolean allow = "anyone".equals(options.getString("muc#roomconfig_allowpm"));
5032 options.putString("allow_private_messages", allow ? "1" : "0");
5033 options.putString("allow_private_messages_from_visitors", allow ? "anyone" : "nobody");
5034 }
5035 final var account = conversation.getAccount();
5036 final Iq request = new Iq(Iq.Type.GET);
5037 request.setTo(conversation.getJid().asBareJid());
5038 request.query("http://jabber.org/protocol/muc#owner");
5039 sendIqPacket(
5040 account,
5041 request,
5042 response -> {
5043 if (response.getType() == Iq.Type.RESULT) {
5044 final Data data =
5045 Data.parse(response.query().findChild("x", Namespace.DATA));
5046 data.submit(options);
5047 final Iq set = new Iq(Iq.Type.SET);
5048 set.setTo(conversation.getJid().asBareJid());
5049 set.query("http://jabber.org/protocol/muc#owner").addChild(data);
5050 sendIqPacket(
5051 account,
5052 set,
5053 packet -> {
5054 if (callback != null) {
5055 if (packet.getType() == Iq.Type.RESULT) {
5056 callback.onPushSucceeded();
5057 } else {
5058 Log.d(Config.LOGTAG, "failed: " + packet.toString());
5059 callback.onPushFailed();
5060 }
5061 }
5062 });
5063 } else {
5064 if (callback != null) {
5065 callback.onPushFailed();
5066 }
5067 }
5068 });
5069 }
5070
5071 public void pushSubjectToConference(final Conversation conference, final String subject) {
5072 final var packet =
5073 this.getMessageGenerator()
5074 .conferenceSubject(conference, StringUtils.nullOnEmpty(subject));
5075 this.sendMessagePacket(conference.getAccount(), packet);
5076 }
5077
5078 public void requestVoice(final Account account, final Jid jid) {
5079 final var packet = this.getMessageGenerator().requestVoice(jid);
5080 this.sendMessagePacket(account, packet);
5081 }
5082
5083 public void changeAffiliationInConference(
5084 final Conversation conference,
5085 Jid user,
5086 final MucOptions.Affiliation affiliation,
5087 final OnAffiliationChanged callback) {
5088 final Jid jid = user.asBareJid();
5089 final Iq request =
5090 this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
5091 sendIqPacket(
5092 conference.getAccount(),
5093 request,
5094 (response) -> {
5095 if (response.getType() == Iq.Type.RESULT) {
5096 final var mucOptions = conference.getMucOptions();
5097 mucOptions.changeAffiliation(jid, affiliation);
5098 getAvatarService().clear(mucOptions);
5099 if (callback != null) {
5100 callback.onAffiliationChangedSuccessful(jid);
5101 } else {
5102 Log.d(
5103 Config.LOGTAG,
5104 "changed affiliation of " + user + " to " + affiliation);
5105 }
5106 } else if (callback != null) {
5107 callback.onAffiliationChangeFailed(
5108 jid, R.string.could_not_change_affiliation);
5109 } else {
5110 Log.d(Config.LOGTAG, "unable to change affiliation");
5111 }
5112 });
5113 }
5114
5115 public void changeRoleInConference(
5116 final Conversation conference, final String nick, MucOptions.Role role) {
5117 final var account = conference.getAccount();
5118 final Iq request = this.mIqGenerator.changeRole(conference, nick, role.toString());
5119 sendIqPacket(
5120 account,
5121 request,
5122 (packet) -> {
5123 if (packet.getType() != Iq.Type.RESULT) {
5124 Log.d(
5125 Config.LOGTAG,
5126 account.getJid().asBareJid() + " unable to change role of " + nick);
5127 }
5128 });
5129 }
5130
5131 public void moderateMessage(final Account account, final Message m, final String reason) {
5132 final var request = this.mIqGenerator.moderateMessage(account, m, reason);
5133 sendIqPacket(account, request, (packet) -> {
5134 if (packet.getType() != Iq.Type.RESULT) {
5135 showErrorToastInUi(R.string.unable_to_moderate);
5136 Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to moderate: " + packet);
5137 }
5138 });
5139 }
5140
5141 public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) {
5142 final Iq request = new Iq(Iq.Type.SET);
5143 request.setTo(conversation.getJid().asBareJid());
5144 request.query("http://jabber.org/protocol/muc#owner").addChild("destroy");
5145 sendIqPacket(
5146 conversation.getAccount(),
5147 request,
5148 response -> {
5149 if (response.getType() == Iq.Type.RESULT) {
5150 if (callback != null) {
5151 callback.onRoomDestroySucceeded();
5152 }
5153 } else if (response.getType() == Iq.Type.ERROR) {
5154 if (callback != null) {
5155 callback.onRoomDestroyFailed();
5156 }
5157 }
5158 });
5159 }
5160
5161 private void disconnect(final Account account, boolean force) {
5162 final XmppConnection connection = account.getXmppConnection();
5163 if (connection == null) {
5164 return;
5165 }
5166 if (!force) {
5167 final List<Conversation> conversations = getConversations();
5168 for (Conversation conversation : conversations) {
5169 if (conversation.getAccount() == account) {
5170 if (conversation.getMode() == Conversation.MODE_MULTI) {
5171 leaveMuc(conversation, true);
5172 }
5173 }
5174 }
5175 sendOfflinePresence(account);
5176 }
5177 connection.disconnect(force);
5178 }
5179
5180 @Override
5181 public IBinder onBind(Intent intent) {
5182 return mBinder;
5183 }
5184
5185 public void deleteMessage(Message message) {
5186 mScheduledMessages.remove(message.getUuid());
5187 databaseBackend.deleteMessage(message.getUuid());
5188 ((Conversation) message.getConversation()).remove(message);
5189 updateConversationUi();
5190 }
5191
5192 public void updateMessage(Message message) {
5193 updateMessage(message, true);
5194 }
5195
5196 public void updateMessage(Message message, boolean includeBody) {
5197 databaseBackend.updateMessage(message, includeBody);
5198 updateConversationUi();
5199 }
5200
5201 public void createMessageAsync(final Message message) {
5202 mDatabaseWriterExecutor.execute(() -> databaseBackend.createMessage(message));
5203 }
5204
5205 public void updateMessage(Message message, String uuid) {
5206 if (!databaseBackend.updateMessage(message, uuid)) {
5207 Log.e(Config.LOGTAG, "error updated message in DB after edit");
5208 }
5209 updateConversationUi();
5210 }
5211
5212 public void syncDirtyContacts(Account account) {
5213 for (Contact contact : account.getRoster().getContacts()) {
5214 if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
5215 pushContactToServer(contact);
5216 }
5217 if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
5218 deleteContactOnServer(contact);
5219 }
5220 }
5221 }
5222
5223 protected void unregisterPhoneAccounts(final Account account) {
5224 for (final Contact contact : account.getRoster().getContacts()) {
5225 if (!contact.showInRoster()) {
5226 contact.unregisterAsPhoneAccount(this);
5227 }
5228 }
5229 }
5230
5231 public void createContact(final Contact contact, final boolean autoGrant) {
5232 createContact(contact, autoGrant, null);
5233 }
5234
5235 public void createContact(
5236 final Contact contact, final boolean autoGrant, final String preAuth) {
5237 if (autoGrant) {
5238 contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
5239 contact.setOption(Contact.Options.ASKING);
5240 }
5241 pushContactToServer(contact, preAuth);
5242 }
5243
5244 public void pushContactToServer(final Contact contact) {
5245 pushContactToServer(contact, null);
5246 }
5247
5248 private void pushContactToServer(final Contact contact, final String preAuth) {
5249 contact.resetOption(Contact.Options.DIRTY_DELETE);
5250 contact.setOption(Contact.Options.DIRTY_PUSH);
5251 final Account account = contact.getAccount();
5252 if (account.getStatus() == Account.State.ONLINE) {
5253 final boolean ask = contact.getOption(Contact.Options.ASKING);
5254 final boolean sendUpdates =
5255 contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
5256 && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
5257 final Iq iq = new Iq(Iq.Type.SET);
5258 iq.query(Namespace.ROSTER).addChild(contact.asElement());
5259 account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
5260 if (sendUpdates) {
5261 sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
5262 }
5263 if (ask) {
5264 sendPresencePacket(
5265 account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth));
5266 }
5267 } else {
5268 syncRoster(contact.getAccount());
5269 }
5270 }
5271
5272 public void publishMucAvatar(
5273 final Conversation conversation, final Uri image, final OnAvatarPublication callback) {
5274 new Thread(
5275 () -> {
5276 final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
5277 final int size = Config.AVATAR_SIZE;
5278 final Avatar avatar =
5279 getFileBackend().getPepAvatar(image, size, format);
5280 if (avatar != null) {
5281 if (!getFileBackend().save(avatar)) {
5282 callback.onAvatarPublicationFailed(
5283 R.string.error_saving_avatar);
5284 return;
5285 }
5286 avatar.owner = conversation.getJid().asBareJid();
5287 publishMucAvatar(conversation, avatar, callback);
5288 } else {
5289 callback.onAvatarPublicationFailed(
5290 R.string.error_publish_avatar_converting);
5291 }
5292 })
5293 .start();
5294 }
5295
5296 public void publishAvatarAsync(
5297 final Account account,
5298 final Uri image,
5299 final boolean open,
5300 final OnAvatarPublication callback) {
5301 new Thread(() -> publishAvatar(account, image, open, callback)).start();
5302 }
5303
5304 private void publishAvatar(
5305 final Account account,
5306 final Uri image,
5307 final boolean open,
5308 final OnAvatarPublication callback) {
5309 final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
5310 final int size = Config.AVATAR_SIZE;
5311 final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
5312 if (avatar != null) {
5313 if (!getFileBackend().save(avatar)) {
5314 Log.d(Config.LOGTAG, "unable to save vcard");
5315 callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
5316 return;
5317 }
5318 publishAvatar(account, avatar, open, callback);
5319 } else {
5320 callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
5321 }
5322 }
5323
5324 private void publishMucAvatar(
5325 Conversation conversation, Avatar avatar, OnAvatarPublication callback) {
5326 final var account = conversation.getAccount();
5327 final Iq retrieve = mIqGenerator.retrieveVcardAvatar(avatar);
5328 sendIqPacket(
5329 account,
5330 retrieve,
5331 (response) -> {
5332 boolean itemNotFound =
5333 response.getType() == Iq.Type.ERROR
5334 && response.hasChild("error")
5335 && response.findChild("error").hasChild("item-not-found");
5336 if (response.getType() == Iq.Type.RESULT || itemNotFound) {
5337 Element vcard = response.findChild("vCard", "vcard-temp");
5338 if (vcard == null) {
5339 vcard = new Element("vCard", "vcard-temp");
5340 }
5341 Element photo = vcard.findChild("PHOTO");
5342 if (photo == null) {
5343 photo = vcard.addChild("PHOTO");
5344 }
5345 photo.clearChildren();
5346 photo.addChild("TYPE").setContent(avatar.type);
5347 photo.addChild("BINVAL").setContent(avatar.image);
5348 final Iq publication = new Iq(Iq.Type.SET);
5349 publication.setTo(conversation.getJid().asBareJid());
5350 publication.addChild(vcard);
5351 sendIqPacket(
5352 account,
5353 publication,
5354 (publicationResponse) -> {
5355 if (publicationResponse.getType() == Iq.Type.RESULT) {
5356 callback.onAvatarPublicationSucceeded();
5357 } else {
5358 Log.d(
5359 Config.LOGTAG,
5360 "failed to publish vcard "
5361 + publicationResponse.getErrorCondition());
5362 callback.onAvatarPublicationFailed(
5363 R.string.error_publish_avatar_server_reject);
5364 }
5365 });
5366 } else {
5367 Log.d(Config.LOGTAG, "failed to request vcard " + response);
5368 callback.onAvatarPublicationFailed(
5369 R.string.error_publish_avatar_no_server_support);
5370 }
5371 });
5372 }
5373
5374 public void publishAvatar(
5375 final Account account,
5376 final Avatar avatar,
5377 final boolean open,
5378 final OnAvatarPublication callback) {
5379 final Bundle options;
5380 if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
5381 options = open ? PublishOptions.openAccess() : PublishOptions.presenceAccess();
5382 } else {
5383 options = null;
5384 }
5385 publishAvatar(account, avatar, options, true, callback);
5386 }
5387
5388 public void publishAvatar(
5389 Account account,
5390 final Avatar avatar,
5391 final Bundle options,
5392 final boolean retry,
5393 final OnAvatarPublication callback) {
5394 Log.d(
5395 Config.LOGTAG,
5396 account.getJid().asBareJid() + ": publishing avatar. options=" + options);
5397 final Iq packet = this.mIqGenerator.publishAvatar(avatar, options);
5398 this.sendIqPacket(
5399 account,
5400 packet,
5401 result -> {
5402 if (result.getType() == Iq.Type.RESULT) {
5403 publishAvatarMetadata(account, avatar, options, true, callback);
5404 } else if (retry && PublishOptions.preconditionNotMet(result)) {
5405 pushNodeConfiguration(
5406 account,
5407 Namespace.AVATAR_DATA,
5408 options,
5409 new OnConfigurationPushed() {
5410 @Override
5411 public void onPushSucceeded() {
5412 Log.d(
5413 Config.LOGTAG,
5414 account.getJid().asBareJid()
5415 + ": changed node configuration for avatar"
5416 + " node");
5417 publishAvatar(account, avatar, options, false, callback);
5418 }
5419
5420 @Override
5421 public void onPushFailed() {
5422 Log.d(
5423 Config.LOGTAG,
5424 account.getJid().asBareJid()
5425 + ": unable to change node configuration"
5426 + " for avatar node");
5427 publishAvatar(account, avatar, null, false, callback);
5428 }
5429 });
5430 } else {
5431 Element error = result.findChild("error");
5432 Log.d(
5433 Config.LOGTAG,
5434 account.getJid().asBareJid()
5435 + ": server rejected avatar "
5436 + (avatar.size / 1024)
5437 + "KiB "
5438 + (error != null ? error.toString() : ""));
5439 if (callback != null) {
5440 callback.onAvatarPublicationFailed(
5441 R.string.error_publish_avatar_server_reject);
5442 }
5443 }
5444 });
5445 }
5446
5447 public void publishAvatarMetadata(
5448 Account account,
5449 final Avatar avatar,
5450 final Bundle options,
5451 final boolean retry,
5452 final OnAvatarPublication callback) {
5453 final Iq packet =
5454 XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options);
5455 sendIqPacket(
5456 account,
5457 packet,
5458 result -> {
5459 if (result.getType() == Iq.Type.RESULT) {
5460 if (account.setAvatar(avatar.getFilename())) {
5461 getAvatarService().clear(account);
5462 databaseBackend.updateAccount(account);
5463 notifyAccountAvatarHasChanged(account);
5464 }
5465 Log.d(
5466 Config.LOGTAG,
5467 account.getJid().asBareJid()
5468 + ": published avatar "
5469 + (avatar.size / 1024)
5470 + "KiB");
5471 if (callback != null) {
5472 callback.onAvatarPublicationSucceeded();
5473 }
5474 } else if (retry && PublishOptions.preconditionNotMet(result)) {
5475 pushNodeConfiguration(
5476 account,
5477 Namespace.AVATAR_METADATA,
5478 options,
5479 new OnConfigurationPushed() {
5480 @Override
5481 public void onPushSucceeded() {
5482 Log.d(
5483 Config.LOGTAG,
5484 account.getJid().asBareJid()
5485 + ": changed node configuration for avatar"
5486 + " meta data node");
5487 publishAvatarMetadata(
5488 account, avatar, options, false, callback);
5489 }
5490
5491 @Override
5492 public void onPushFailed() {
5493 Log.d(
5494 Config.LOGTAG,
5495 account.getJid().asBareJid()
5496 + ": unable to change node configuration"
5497 + " for avatar meta data node");
5498 publishAvatarMetadata(
5499 account, avatar, null, false, callback);
5500 }
5501 });
5502 } else {
5503 if (callback != null) {
5504 callback.onAvatarPublicationFailed(
5505 R.string.error_publish_avatar_server_reject);
5506 }
5507 }
5508 });
5509 }
5510
5511 public void republishAvatarIfNeeded(final Account account) {
5512 if (account.getAxolotlService().isPepBroken()) {
5513 Log.d(
5514 Config.LOGTAG,
5515 account.getJid().asBareJid()
5516 + ": skipping republication of avatar because pep is broken");
5517 return;
5518 }
5519 final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
5520 this.sendIqPacket(
5521 account,
5522 packet,
5523 new Consumer<Iq>() {
5524
5525 private Avatar parseAvatar(Iq packet) {
5526 Element pubsub =
5527 packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
5528 if (pubsub != null) {
5529 Element items = pubsub.findChild("items");
5530 if (items != null) {
5531 return Avatar.parseMetadata(items);
5532 }
5533 }
5534 return null;
5535 }
5536
5537 private boolean errorIsItemNotFound(Iq packet) {
5538 Element error = packet.findChild("error");
5539 return packet.getType() == Iq.Type.ERROR
5540 && error != null
5541 && error.hasChild("item-not-found");
5542 }
5543
5544 @Override
5545 public void accept(final Iq packet) {
5546 if (packet.getType() == Iq.Type.RESULT || errorIsItemNotFound(packet)) {
5547 final Avatar serverAvatar = parseAvatar(packet);
5548 if (serverAvatar == null && account.getAvatar() != null) {
5549 final Avatar avatar =
5550 fileBackend.getStoredPepAvatar(account.getAvatar());
5551 if (avatar != null) {
5552 Log.d(
5553 Config.LOGTAG,
5554 account.getJid().asBareJid()
5555 + ": avatar on server was null. republishing");
5556 // publishing as 'open' - old server (that requires
5557 // republication) likely doesn't support access models anyway
5558 publishAvatar(
5559 account,
5560 fileBackend.getStoredPepAvatar(account.getAvatar()),
5561 true,
5562 null);
5563 } else {
5564 Log.e(
5565 Config.LOGTAG,
5566 account.getJid().asBareJid()
5567 + ": error rereading avatar");
5568 }
5569 }
5570 }
5571 }
5572 });
5573 }
5574
5575 public void cancelAvatarFetches(final Account account) {
5576 synchronized (mInProgressAvatarFetches) {
5577 for (final Iterator<String> iterator = mInProgressAvatarFetches.iterator();
5578 iterator.hasNext(); ) {
5579 final String KEY = iterator.next();
5580 if (KEY.startsWith(account.getJid().asBareJid() + "_")) {
5581 iterator.remove();
5582 }
5583 }
5584 }
5585 }
5586
5587 public void fetchAvatar(Account account, Avatar avatar) {
5588 fetchAvatar(account, avatar, null);
5589 }
5590
5591 public void fetchAvatar(
5592 Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5593 if (databaseBackend.isBlockedMedia(avatar.cid())) {
5594 if (callback != null) callback.error(0, null);
5595 return;
5596 }
5597
5598 final String KEY = generateFetchKey(account, avatar);
5599 synchronized (this.mInProgressAvatarFetches) {
5600 if (mInProgressAvatarFetches.add(KEY)) {
5601 switch (avatar.origin) {
5602 case PEP:
5603 this.mInProgressAvatarFetches.add(KEY);
5604 fetchAvatarPep(account, avatar, callback);
5605 break;
5606 case VCARD:
5607 this.mInProgressAvatarFetches.add(KEY);
5608 fetchAvatarVcard(account, avatar, callback);
5609 break;
5610 }
5611 } else if (avatar.origin == Avatar.Origin.PEP) {
5612 mOmittedPepAvatarFetches.add(KEY);
5613 } else {
5614 Log.d(
5615 Config.LOGTAG,
5616 account.getJid().asBareJid()
5617 + ": already fetching "
5618 + avatar.origin
5619 + " avatar for "
5620 + avatar.owner);
5621 }
5622 }
5623 }
5624
5625 private void fetchAvatarPep(
5626 final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5627 final Iq packet = this.mIqGenerator.retrievePepAvatar(avatar);
5628 sendIqPacket(
5629 account,
5630 packet,
5631 (result) -> {
5632 synchronized (mInProgressAvatarFetches) {
5633 mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
5634 }
5635 final String ERROR =
5636 account.getJid().asBareJid()
5637 + ": fetching avatar for "
5638 + avatar.owner
5639 + " failed ";
5640 if (result.getType() == Iq.Type.RESULT) {
5641 avatar.image = IqParser.avatarData(result);
5642 if (avatar.image != null) {
5643 if (getFileBackend().save(avatar)) {
5644 if (account.getJid().asBareJid().equals(avatar.owner)) {
5645 if (account.setAvatar(avatar.getFilename())) {
5646 databaseBackend.updateAccount(account);
5647 }
5648 getAvatarService().clear(account);
5649 updateConversationUi();
5650 updateAccountUi();
5651 } else {
5652 final Contact contact =
5653 account.getRoster().getContact(avatar.owner);
5654 contact.setAvatar(avatar);
5655 syncRoster(account);
5656 getAvatarService().clear(contact);
5657 updateConversationUi();
5658 updateRosterUi(UpdateRosterReason.AVATAR);
5659 }
5660 if (callback != null) {
5661 callback.success(avatar);
5662 }
5663 Log.d(
5664 Config.LOGTAG,
5665 account.getJid().asBareJid()
5666 + ": successfully fetched pep avatar for "
5667 + avatar.owner);
5668 return;
5669 }
5670 } else {
5671
5672 Log.d(Config.LOGTAG, ERROR + "(parsing error)");
5673 }
5674 } else {
5675 Element error = result.findChild("error");
5676 if (error == null) {
5677 Log.d(Config.LOGTAG, ERROR + "(server error)");
5678 } else {
5679 Log.d(Config.LOGTAG, ERROR + error.toString());
5680 }
5681 }
5682 if (callback != null) {
5683 callback.error(0, null);
5684 }
5685 });
5686 }
5687
5688 private void fetchAvatarVcard(
5689 final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
5690 final Iq packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
5691 this.sendIqPacket(
5692 account,
5693 packet,
5694 response -> {
5695 final boolean previouslyOmittedPepFetch;
5696 synchronized (mInProgressAvatarFetches) {
5697 final String KEY = generateFetchKey(account, avatar);
5698 mInProgressAvatarFetches.remove(KEY);
5699 previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY);
5700 }
5701 if (response.getType() == Iq.Type.RESULT) {
5702 Element vCard = response.findChild("vCard", "vcard-temp");
5703 Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
5704 String image = photo != null ? photo.findChildContent("BINVAL") : null;
5705 if (image != null) {
5706 avatar.image = image;
5707 if (getFileBackend().save(avatar)) {
5708 Log.d(
5709 Config.LOGTAG,
5710 account.getJid().asBareJid()
5711 + ": successfully fetched vCard avatar for "
5712 + avatar.owner
5713 + " omittedPep="
5714 + previouslyOmittedPepFetch);
5715 if (avatar.owner.isBareJid()) {
5716 if (account.getJid().asBareJid().equals(avatar.owner)
5717 && account.getAvatar() == null) {
5718 Log.d(
5719 Config.LOGTAG,
5720 account.getJid().asBareJid()
5721 + ": had no avatar. replacing with vcard");
5722 account.setAvatar(avatar.getFilename());
5723 databaseBackend.updateAccount(account);
5724 getAvatarService().clear(account);
5725 updateAccountUi();
5726 } else {
5727 final Contact contact =
5728 account.getRoster().getContact(avatar.owner);
5729 contact.setAvatar(avatar, previouslyOmittedPepFetch);
5730 syncRoster(account);
5731 getAvatarService().clear(contact);
5732 updateRosterUi(UpdateRosterReason.AVATAR);
5733 }
5734 updateConversationUi();
5735 } else {
5736 Conversation conversation =
5737 find(account, avatar.owner.asBareJid());
5738 if (conversation != null
5739 && conversation.getMode() == Conversation.MODE_MULTI) {
5740 MucOptions.User user =
5741 conversation
5742 .getMucOptions()
5743 .findUserByFullJid(avatar.owner);
5744 if (user != null) {
5745 if (user.setAvatar(avatar)) {
5746 getAvatarService().clear(user);
5747 updateConversationUi();
5748 updateMucRosterUi();
5749 }
5750 if (user.getRealJid() != null) {
5751 Contact contact =
5752 account.getRoster()
5753 .getContact(user.getRealJid());
5754 contact.setAvatar(avatar);
5755 syncRoster(account);
5756 getAvatarService().clear(contact);
5757 updateRosterUi(UpdateRosterReason.AVATAR);
5758 }
5759 }
5760 }
5761 }
5762 }
5763 }
5764 }
5765 });
5766 }
5767
5768 public void checkForAvatar(final Account account, final UiCallback<Avatar> callback) {
5769 final Iq packet = this.mIqGenerator.retrieveAvatarMetaData(null);
5770 this.sendIqPacket(
5771 account,
5772 packet,
5773 response -> {
5774 if (response.getType() == Iq.Type.RESULT) {
5775 Element pubsub =
5776 response.findChild("pubsub", "http://jabber.org/protocol/pubsub");
5777 if (pubsub != null) {
5778 Element items = pubsub.findChild("items");
5779 if (items != null) {
5780 Avatar avatar = Avatar.parseMetadata(items);
5781 if (avatar != null) {
5782 avatar.owner = account.getJid().asBareJid();
5783 if (fileBackend.isAvatarCached(avatar)) {
5784 if (account.setAvatar(avatar.getFilename())) {
5785 databaseBackend.updateAccount(account);
5786 }
5787 getAvatarService().clear(account);
5788 callback.success(avatar);
5789 } else {
5790 fetchAvatarPep(account, avatar, callback);
5791 }
5792 return;
5793 }
5794 }
5795 }
5796 }
5797 callback.error(0, null);
5798 });
5799 }
5800
5801 public void notifyAccountAvatarHasChanged(final Account account) {
5802 final XmppConnection connection = account.getXmppConnection();
5803 if (connection != null && connection.getFeatures().bookmarksConversion()) {
5804 Log.d(
5805 Config.LOGTAG,
5806 account.getJid().asBareJid()
5807 + ": avatar changed. resending presence to online group chats");
5808 for (Conversation conversation : conversations) {
5809 if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) {
5810 presenceToMuc(conversation);
5811 }
5812 }
5813 }
5814 }
5815
5816 public void fetchVcard4(Account account, final Contact contact, final Consumer<Element> callback) {
5817 final var packet = this.mIqGenerator.retrieveVcard4(contact.getJid());
5818 sendIqPacket(account, packet, (result) -> {
5819 if (result.getType() == Iq.Type.RESULT) {
5820 final Element item = IqParser.getItem(result);
5821 if (item != null) {
5822 final Element vcard4 = item.findChild("vcard", Namespace.VCARD4);
5823 if (vcard4 != null) {
5824 if (callback != null) {
5825 callback.accept(vcard4);
5826 }
5827 return;
5828 }
5829 }
5830 } else {
5831 Element error = result.findChild("error");
5832 if (error == null) {
5833 Log.d(Config.LOGTAG, "fetchVcard4 (server error)");
5834 } else {
5835 Log.d(Config.LOGTAG, "fetchVcard4 " + error.toString());
5836 }
5837 }
5838 if (callback != null) {
5839 callback.accept(null);
5840 }
5841
5842 });
5843 }
5844
5845 public void deleteContactOnServer(Contact contact) {
5846 contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
5847 contact.resetOption(Contact.Options.DIRTY_PUSH);
5848 contact.setOption(Contact.Options.DIRTY_DELETE);
5849 Account account = contact.getAccount();
5850 if (account.getStatus() == Account.State.ONLINE) {
5851 final Iq iq = new Iq(Iq.Type.SET);
5852 Element item = iq.query(Namespace.ROSTER).addChild("item");
5853 item.setAttribute("jid", contact.getJid());
5854 item.setAttribute("subscription", "remove");
5855 account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
5856 }
5857 }
5858
5859 public void updateConversation(final Conversation conversation) {
5860 mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation));
5861 }
5862
5863 private void reconnectAccount(
5864 final Account account, final boolean force, final boolean interactive) {
5865 synchronized (account) {
5866 final XmppConnection existingConnection = account.getXmppConnection();
5867 final XmppConnection connection;
5868 if (existingConnection != null) {
5869 connection = existingConnection;
5870 } else if (account.isConnectionEnabled()) {
5871 connection = createConnection(account);
5872 account.setXmppConnection(connection);
5873 } else {
5874 return;
5875 }
5876 final boolean hasInternet = hasInternetConnection();
5877 if (account.isConnectionEnabled() && hasInternet) {
5878 if (!force) {
5879 disconnect(account, false);
5880 }
5881 Thread thread = new Thread(connection);
5882 connection.setInteractive(interactive);
5883 connection.prepareNewConnection();
5884 connection.interrupt();
5885 thread.start();
5886 scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
5887 } else {
5888 disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
5889 account.getRoster().clearPresences();
5890 connection.resetEverything();
5891 final AxolotlService axolotlService = account.getAxolotlService();
5892 if (axolotlService != null) {
5893 axolotlService.resetBrokenness();
5894 }
5895 if (!hasInternet) {
5896 account.setStatus(Account.State.NO_INTERNET);
5897 }
5898 }
5899 }
5900 }
5901
5902 public void reconnectAccountInBackground(final Account account) {
5903 new Thread(() -> reconnectAccount(account, false, true)).start();
5904 }
5905
5906 public void invite(final Conversation conversation, final Jid contact) {
5907 Log.d(
5908 Config.LOGTAG,
5909 conversation.getAccount().getJid().asBareJid()
5910 + ": inviting "
5911 + contact
5912 + " to "
5913 + conversation.getJid().asBareJid());
5914 final MucOptions.User user =
5915 conversation.getMucOptions().findUserByRealJid(contact.asBareJid());
5916 if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) {
5917 changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null);
5918 }
5919 final var packet = mMessageGenerator.invite(conversation, contact);
5920 sendMessagePacket(conversation.getAccount(), packet);
5921 }
5922
5923 public void directInvite(Conversation conversation, Jid jid) {
5924 final var packet = mMessageGenerator.directInvite(conversation, jid);
5925 sendMessagePacket(conversation.getAccount(), packet);
5926 }
5927
5928 public void resetSendingToWaiting(Account account) {
5929 for (Conversation conversation : getConversations()) {
5930 if (conversation.getAccount() == account) {
5931 conversation.findUnsentTextMessages(
5932 message -> markMessage(message, Message.STATUS_WAITING));
5933 }
5934 }
5935 }
5936
5937 public Message markMessage(
5938 final Account account, final Jid recipient, final String uuid, final int status) {
5939 return markMessage(account, recipient, uuid, status, null);
5940 }
5941
5942 public Message markMessage(
5943 final Account account,
5944 final Jid recipient,
5945 final String uuid,
5946 final int status,
5947 String errorMessage) {
5948 if (uuid == null) {
5949 return null;
5950 }
5951 for (Conversation conversation : getConversations()) {
5952 if (conversation.getJid().asBareJid().equals(recipient)
5953 && conversation.getAccount() == account) {
5954 final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
5955 if (message != null) {
5956 markMessage(message, status, errorMessage);
5957 }
5958 return message;
5959 }
5960 }
5961 return null;
5962 }
5963
5964 public boolean markMessage(
5965 final Conversation conversation,
5966 final String uuid,
5967 final int status,
5968 final String serverMessageId) {
5969 return markMessage(conversation, uuid, status, serverMessageId, null, null, null, null, null);
5970 }
5971
5972 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) {
5973 if (uuid == null) {
5974 return false;
5975 } else {
5976 final Message message = conversation.findSentMessageWithUuid(uuid);
5977 if (message != null) {
5978 if (message.getServerMsgId() == null) {
5979 message.setServerMsgId(serverMessageId);
5980 }
5981 if (message.getEncryption() == Message.ENCRYPTION_NONE && (body != null || html != null || subject != null || thread != null || attachments != null)) {
5982 message.setBody(body.content);
5983 if (body.count > 1) {
5984 message.setBodyLanguage(body.language);
5985 }
5986 message.setHtml(html);
5987 message.setSubject(subject);
5988 message.setThread(thread);
5989 if (attachments != null && attachments.isEmpty()) {
5990 message.setRelativeFilePath(null);
5991 message.resetFileParams();
5992 }
5993 markMessage(message, status, null, true);
5994 } else {
5995 markMessage(message, status);
5996 }
5997 return true;
5998 } else {
5999 return false;
6000 }
6001 }
6002 }
6003
6004 public void markMessage(Message message, int status) {
6005 markMessage(message, status, null);
6006 }
6007
6008 public void markMessage(final Message message, final int status, final String errorMessage) {
6009 markMessage(message, status, errorMessage, false);
6010 }
6011
6012 public void markMessage(
6013 final Message message,
6014 final int status,
6015 final String errorMessage,
6016 final boolean includeBody) {
6017 final int oldStatus = message.getStatus();
6018 if (status == Message.STATUS_SEND_FAILED
6019 && (oldStatus == Message.STATUS_SEND_RECEIVED
6020 || oldStatus == Message.STATUS_SEND_DISPLAYED)) {
6021 return;
6022 }
6023 if (status == Message.STATUS_SEND_RECEIVED && oldStatus == Message.STATUS_SEND_DISPLAYED) {
6024 return;
6025 }
6026 message.setErrorMessage(errorMessage);
6027 message.setStatus(status);
6028 databaseBackend.updateMessage(message, includeBody);
6029 updateConversationUi();
6030 if (oldStatus != status && status == Message.STATUS_SEND_FAILED) {
6031 mNotificationService.pushFailedDelivery(message);
6032 }
6033 }
6034
6035 public SharedPreferences getPreferences() {
6036 return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
6037 }
6038
6039 public long getAutomaticMessageDeletionDate() {
6040 final long timeout =
6041 getLongPreference(
6042 AppSettings.AUTOMATIC_MESSAGE_DELETION,
6043 R.integer.automatic_message_deletion);
6044 return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000));
6045 }
6046
6047 public long getLongPreference(String name, @IntegerRes int res) {
6048 long defaultValue = getResources().getInteger(res);
6049 try {
6050 return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue)));
6051 } catch (NumberFormatException e) {
6052 return defaultValue;
6053 }
6054 }
6055
6056 public boolean getBooleanPreference(String name, @BoolRes int res) {
6057 return getPreferences().getBoolean(name, getResources().getBoolean(res));
6058 }
6059
6060 public String getStringPreference(String name, @BoolRes int res) {
6061 return getPreferences().getString(name, getResources().getString(res));
6062 }
6063
6064 public boolean confirmMessages() {
6065 return getBooleanPreference("confirm_messages", R.bool.confirm_messages);
6066 }
6067
6068 public boolean allowMessageCorrection() {
6069 return getBooleanPreference("allow_message_correction", R.bool.allow_message_correction);
6070 }
6071
6072 public boolean sendChatStates() {
6073 return getBooleanPreference("chat_states", R.bool.chat_states);
6074 }
6075
6076 public boolean useTorToConnect() {
6077 return getBooleanPreference("use_tor", R.bool.use_tor);
6078 }
6079
6080 public boolean showExtendedConnectionOptions() {
6081 return getBooleanPreference(AppSettings.SHOW_CONNECTION_OPTIONS, R.bool.show_connection_options);
6082 }
6083
6084 public boolean broadcastLastActivity() {
6085 return getBooleanPreference(AppSettings.BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
6086 }
6087
6088 public int unreadCount() {
6089 int count = 0;
6090 for (Conversation conversation : getConversations()) {
6091 count += conversation.unreadCount(this);
6092 }
6093 return count;
6094 }
6095
6096 private <T> List<T> threadSafeList(Set<T> set) {
6097 synchronized (LISTENER_LOCK) {
6098 return set.isEmpty() ? Collections.emptyList() : new ArrayList<>(set);
6099 }
6100 }
6101
6102 public void showErrorToastInUi(int resId) {
6103 for (OnShowErrorToast listener : threadSafeList(this.mOnShowErrorToasts)) {
6104 listener.onShowErrorToast(resId);
6105 }
6106 }
6107
6108 public void updateConversationUi() {
6109 updateConversationUi(false);
6110 }
6111
6112 public void updateConversationUi(boolean newCaps) {
6113 for (OnConversationUpdate listener : threadSafeList(this.mOnConversationUpdates)) {
6114 listener.onConversationUpdate(newCaps);
6115 }
6116 }
6117
6118 public void notifyJingleRtpConnectionUpdate(
6119 final Account account,
6120 final Jid with,
6121 final String sessionId,
6122 final RtpEndUserState state) {
6123 for (OnJingleRtpConnectionUpdate listener :
6124 threadSafeList(this.onJingleRtpConnectionUpdate)) {
6125 listener.onJingleRtpConnectionUpdate(account, with, sessionId, state);
6126 }
6127 }
6128
6129 public void notifyJingleRtpConnectionUpdate(
6130 CallIntegration.AudioDevice selectedAudioDevice,
6131 Set<CallIntegration.AudioDevice> availableAudioDevices) {
6132 for (OnJingleRtpConnectionUpdate listener :
6133 threadSafeList(this.onJingleRtpConnectionUpdate)) {
6134 listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
6135 }
6136 }
6137
6138 public void updateAccountUi() {
6139 for (final OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) {
6140 listener.onAccountUpdate();
6141 }
6142 }
6143
6144 public void updateRosterUi(final UpdateRosterReason reason) {
6145 if (reason == UpdateRosterReason.PRESENCE) throw new IllegalArgumentException("PRESENCE must also come with a contact");
6146 updateRosterUi(reason, null);
6147 }
6148
6149 public void updateRosterUi(final UpdateRosterReason reason, final Contact contact) {
6150 for (OnRosterUpdate listener : threadSafeList(this.mOnRosterUpdates)) {
6151 listener.onRosterUpdate(reason, contact);
6152 }
6153 }
6154
6155 public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
6156 if (mOnCaptchaRequested.size() > 0) {
6157 DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
6158 Bitmap scaled =
6159 Bitmap.createScaledBitmap(
6160 captcha,
6161 (int) (captcha.getWidth() * metrics.scaledDensity),
6162 (int) (captcha.getHeight() * metrics.scaledDensity),
6163 false);
6164 for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
6165 listener.onCaptchaRequested(account, id, data, scaled);
6166 }
6167 return true;
6168 }
6169 return false;
6170 }
6171
6172 public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
6173 for (OnUpdateBlocklist listener : threadSafeList(this.mOnUpdateBlocklist)) {
6174 listener.OnUpdateBlocklist(status);
6175 }
6176 }
6177
6178 public void updateMucRosterUi() {
6179 for (OnMucRosterUpdate listener : threadSafeList(this.mOnMucRosterUpdate)) {
6180 listener.onMucRosterUpdate();
6181 }
6182 }
6183
6184 public void keyStatusUpdated(AxolotlService.FetchStatus report) {
6185 for (OnKeyStatusUpdated listener : threadSafeList(this.mOnKeyStatusUpdated)) {
6186 listener.onKeyStatusUpdated(report);
6187 }
6188 }
6189
6190 public Account findAccountByJid(final Jid jid) {
6191 for (final Account account : this.accounts) {
6192 if (account.getJid().asBareJid().equals(jid.asBareJid())) {
6193 return account;
6194 }
6195 }
6196 return null;
6197 }
6198
6199 public Account findAccountByUuid(final String uuid) {
6200 for (Account account : this.accounts) {
6201 if (account.getUuid().equals(uuid)) {
6202 return account;
6203 }
6204 }
6205 return null;
6206 }
6207
6208 public Conversation findConversationByUuid(String uuid) {
6209 for (Conversation conversation : getConversations()) {
6210 if (conversation.getUuid().equals(uuid)) {
6211 return conversation;
6212 }
6213 }
6214 return null;
6215 }
6216
6217 public Conversation findUniqueConversationByJid(XmppUri xmppUri) {
6218 List<Conversation> findings = new ArrayList<>();
6219 for (Conversation c : getConversations()) {
6220 if (c.getAccount().isEnabled()
6221 && c.getJid().asBareJid().equals(xmppUri.getJid().asBareJid())
6222 && ((c.getMode() == Conversational.MODE_MULTI)
6223 == xmppUri.isAction(XmppUri.ACTION_JOIN))) {
6224 findings.add(c);
6225 }
6226 }
6227 return findings.size() == 1 ? findings.get(0) : null;
6228 }
6229
6230 public boolean markRead(final Conversation conversation, boolean dismiss) {
6231 return markRead(conversation, null, dismiss).size() > 0;
6232 }
6233
6234 public void markRead(final Conversation conversation) {
6235 markRead(conversation, null, true);
6236 }
6237
6238 public List<Message> markRead(
6239 final Conversation conversation, String upToUuid, boolean dismiss) {
6240 if (dismiss) {
6241 mNotificationService.clear(conversation);
6242 }
6243 final List<Message> readMessages = conversation.markRead(upToUuid);
6244 if (readMessages.size() > 0) {
6245 Runnable runnable =
6246 () -> {
6247 for (Message message : readMessages) {
6248 databaseBackend.updateMessage(message, false);
6249 }
6250 };
6251 mDatabaseWriterExecutor.execute(runnable);
6252 updateConversationUi();
6253 updateUnreadCountBadge();
6254 return readMessages;
6255 } else {
6256 return readMessages;
6257 }
6258 }
6259
6260 public void markNotificationDismissed(final List<Message> messages) {
6261 Runnable runnable = () -> {
6262 for (final var message : messages) {
6263 message.markNotificationDismissed();
6264 databaseBackend.updateMessage(message, false);
6265 }
6266 };
6267 mDatabaseWriterExecutor.execute(runnable);
6268 }
6269
6270 public synchronized void updateUnreadCountBadge() {
6271 int count = unreadCount();
6272 if (unreadCount != count) {
6273 Log.d(Config.LOGTAG, "update unread count to " + count);
6274 if (count > 0) {
6275 ShortcutBadger.applyCount(getApplicationContext(), count);
6276 } else {
6277 ShortcutBadger.removeCount(getApplicationContext());
6278 }
6279 unreadCount = count;
6280 }
6281 }
6282
6283 public void sendReadMarker(final Conversation conversation, final String upToUuid) {
6284 final boolean isPrivateAndNonAnonymousMuc =
6285 conversation.getMode() == Conversation.MODE_MULTI
6286 && conversation.isPrivateAndNonAnonymous();
6287 final List<Message> readMessages = this.markRead(conversation, upToUuid, true);
6288 if (readMessages.isEmpty()) {
6289 return;
6290 }
6291 final var account = conversation.getAccount();
6292 final var connection = account.getXmppConnection();
6293 updateConversationUi();
6294 final var last =
6295 Iterables.getLast(
6296 Collections2.filter(
6297 readMessages,
6298 m ->
6299 !m.isPrivateMessage()
6300 && m.getStatus() == Message.STATUS_RECEIVED),
6301 null);
6302 if (last == null) {
6303 return;
6304 }
6305
6306 final boolean sendDisplayedMarker =
6307 confirmMessages()
6308 && (last.trusted() || isPrivateAndNonAnonymousMuc)
6309 && last.getRemoteMsgId() != null
6310 && (last.markable || isPrivateAndNonAnonymousMuc);
6311 final boolean serverAssist =
6312 connection != null && connection.getFeatures().mdsServerAssist();
6313
6314 final String stanzaId = last.getServerMsgId();
6315
6316 if (sendDisplayedMarker && serverAssist) {
6317 final var mdsDisplayed = mIqGenerator.mdsDisplayed(stanzaId, conversation);
6318 final var packet = mMessageGenerator.confirm(last);
6319 packet.addChild(mdsDisplayed);
6320 if (!last.isPrivateMessage()) {
6321 packet.setTo(packet.getTo().asBareJid());
6322 }
6323 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server assisted " + packet);
6324 this.sendMessagePacket(account, packet);
6325 } else {
6326 publishMds(last);
6327 // read markers will be sent after MDS to flush the CSI stanza queue
6328 if (sendDisplayedMarker) {
6329 Log.d(
6330 Config.LOGTAG,
6331 conversation.getAccount().getJid().asBareJid()
6332 + ": sending displayed marker to "
6333 + last.getCounterpart().toString());
6334 final var packet = mMessageGenerator.confirm(last);
6335 this.sendMessagePacket(account, packet);
6336 }
6337 }
6338 }
6339
6340 private void publishMds(@Nullable final Message message) {
6341 final String stanzaId = message == null ? null : message.getServerMsgId();
6342 if (Strings.isNullOrEmpty(stanzaId)) {
6343 return;
6344 }
6345 final Conversation conversation;
6346 final var conversational = message.getConversation();
6347 if (conversational instanceof Conversation c) {
6348 conversation = c;
6349 } else {
6350 return;
6351 }
6352 final var account = conversation.getAccount();
6353 final var connection = account.getXmppConnection();
6354 if (connection == null || !connection.getFeatures().mds()) {
6355 return;
6356 }
6357 final Jid itemId;
6358 if (message.isPrivateMessage()) {
6359 itemId = message.getCounterpart();
6360 } else {
6361 itemId = conversation.getJid().asBareJid();
6362 }
6363 Log.d(Config.LOGTAG, "publishing mds for " + itemId + "/" + stanzaId);
6364 publishMds(account, itemId, stanzaId, conversation);
6365 }
6366
6367 private void publishMds(
6368 final Account account,
6369 final Jid itemId,
6370 final String stanzaId,
6371 final Conversation conversation) {
6372 final var item = mIqGenerator.mdsDisplayed(stanzaId, conversation);
6373 pushNodeAndEnforcePublishOptions(
6374 account,
6375 Namespace.MDS_DISPLAYED,
6376 item,
6377 itemId.toString(),
6378 PublishOptions.persistentWhitelistAccessMaxItems());
6379 }
6380
6381 public boolean sendReactions(final Message message, final Collection<String> reactions) {
6382 if (message.isPrivateMessage()) throw new IllegalArgumentException("Reactions to PM not implemented");
6383 if (message.getConversation() instanceof Conversation conversation) {
6384 final var isPrivateMessage = message.isPrivateMessage();
6385 final Jid reactTo;
6386 final boolean typeGroupChat;
6387 final String reactToId;
6388 final Collection<Reaction> combinedReactions;
6389 final var newReactions = new HashSet<>(reactions);
6390 newReactions.removeAll(message.getAggregatedReactions().ourReactions);
6391 if (conversation.getMode() == Conversational.MODE_MULTI && !isPrivateMessage) {
6392 final var mucOptions = conversation.getMucOptions();
6393 if (!mucOptions.participating()) {
6394 Log.d(Config.LOGTAG, "not participating in MUC");
6395 return false;
6396 }
6397 final var self = mucOptions.getSelf();
6398 final String occupantId = self.getOccupantId();
6399 if (Strings.isNullOrEmpty(occupantId)) {
6400 Log.d(Config.LOGTAG, "occupant id not found for reaction in MUC");
6401 return false;
6402 }
6403 final var existingRaw =
6404 ImmutableSet.copyOf(
6405 Collections2.transform(message.getReactions(), r -> r.reaction));
6406 final var reactionsAsExistingVariants =
6407 ImmutableSet.copyOf(
6408 Collections2.transform(
6409 reactions, r -> Emoticons.existingVariant(r, existingRaw)));
6410 if (!reactions.equals(reactionsAsExistingVariants)) {
6411 Log.d(Config.LOGTAG, "modified reactions to existing variants");
6412 }
6413 reactToId = message.getServerMsgId();
6414 reactTo = conversation.getJid().asBareJid();
6415 typeGroupChat = true;
6416 combinedReactions =
6417 Reaction.withMine(
6418 message.getReactions(),
6419 reactionsAsExistingVariants,
6420 false,
6421 self.getFullJid(),
6422 conversation.getAccount().getJid(),
6423 occupantId,
6424 null);
6425 } else {
6426 if (message.isCarbon() || message.getStatus() == Message.STATUS_RECEIVED) {
6427 reactToId = message.getRemoteMsgId();
6428 } else {
6429 reactToId = message.getUuid();
6430 }
6431 typeGroupChat = false;
6432 if (isPrivateMessage) {
6433 reactTo = message.getCounterpart();
6434 } else {
6435 reactTo = conversation.getJid().asBareJid();
6436 }
6437 combinedReactions =
6438 Reaction.withFrom(
6439 message.getReactions(),
6440 reactions,
6441 false,
6442 conversation.getAccount().getJid(),
6443 null);
6444 }
6445 if (reactTo == null || Strings.isNullOrEmpty(reactToId)) {
6446 return false;
6447 }
6448
6449 final var packet =
6450 mMessageGenerator.reaction(reactTo, typeGroupChat, message, reactToId, reactions);
6451
6452 final var quote = QuoteHelper.quote(MessageUtils.prepareQuote(message)) + "\n";
6453 final var body = quote + String.join(" ", newReactions);
6454 if (conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL && newReactions.size() > 0) {
6455 FILE_ATTACHMENT_EXECUTOR.execute(() -> {
6456 XmppAxolotlMessage axolotlMessage = conversation.getAccount().getAxolotlService().encrypt(body, conversation);
6457 packet.setAxolotlMessage(axolotlMessage.toElement());
6458 packet.addChild("encryption", "urn:xmpp:eme:0")
6459 .setAttribute("name", "OMEMO")
6460 .setAttribute("namespace", AxolotlService.PEP_PREFIX);
6461 sendMessagePacket(conversation.getAccount(), packet);
6462 message.setReactions(combinedReactions);
6463 updateMessage(message, false);
6464 });
6465 } else if (conversation.getNextEncryption() == Message.ENCRYPTION_NONE || newReactions.size() < 1) {
6466 if (newReactions.size() > 0) {
6467 packet.setBody(body);
6468
6469 packet.addChild("reply", "urn:xmpp:reply:0")
6470 .setAttribute("to", message.getCounterpart())
6471 .setAttribute("id", reactToId);
6472 final var replyFallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reply:0");
6473 replyFallback.addChild("body", "urn:xmpp:fallback:0")
6474 .setAttribute("start", "0")
6475 .setAttribute("end", "" + quote.codePointCount(0, quote.length()));
6476
6477 final var fallback = packet.addChild("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reactions:0");
6478 fallback.addChild("body", "urn:xmpp:fallback:0");
6479 }
6480
6481 sendMessagePacket(conversation.getAccount(), packet);
6482 message.setReactions(combinedReactions);
6483 updateMessage(message, false);
6484 }
6485
6486 return true;
6487 } else {
6488 return false;
6489 }
6490 }
6491
6492 public MemorizingTrustManager getMemorizingTrustManager() {
6493 return this.mMemorizingTrustManager;
6494 }
6495
6496 public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
6497 this.mMemorizingTrustManager = trustManager;
6498 }
6499
6500 public void updateMemorizingTrustManager() {
6501 final MemorizingTrustManager trustManager;
6502 if (appSettings.isTrustSystemCAStore()) {
6503 trustManager = new MemorizingTrustManager(getApplicationContext());
6504 } else {
6505 trustManager = new MemorizingTrustManager(getApplicationContext(), null);
6506 }
6507 setMemorizingTrustManager(trustManager);
6508 }
6509
6510 public LruCache<String, Drawable> getDrawableCache() {
6511 return this.mDrawableCache;
6512 }
6513
6514 public Collection<String> getKnownHosts() {
6515 final Set<String> hosts = new HashSet<>();
6516 for (final Account account : getAccounts()) {
6517 hosts.add(account.getServer());
6518 for (final Contact contact : account.getRoster().getContacts()) {
6519 if (contact.showInRoster()) {
6520 final String server = contact.getServer();
6521 if (server != null) {
6522 hosts.add(server);
6523 }
6524 }
6525 }
6526 }
6527 if (Config.QUICKSY_DOMAIN != null) {
6528 hosts.remove(
6529 Config.QUICKSY_DOMAIN
6530 .toString()); // we only want to show this when we type a e164
6531 // number
6532 }
6533 if (Config.MAGIC_CREATE_DOMAIN != null) {
6534 hosts.add(Config.MAGIC_CREATE_DOMAIN);
6535 }
6536 hosts.add("chat.above.im");
6537 return hosts;
6538 }
6539
6540 public Collection<String> getKnownConferenceHosts() {
6541 final Set<String> mucServers = new HashSet<>();
6542 for (final Account account : accounts) {
6543 if (account.getXmppConnection() != null) {
6544 mucServers.addAll(account.getXmppConnection().getMucServers());
6545 for (final Bookmark bookmark : account.getBookmarks()) {
6546 final Jid jid = bookmark.getJid();
6547 final String s = jid == null ? null : jid.getDomain().toString();
6548 if (s != null) {
6549 mucServers.add(s);
6550 }
6551 }
6552 }
6553 }
6554 return mucServers;
6555 }
6556
6557 public void sendMessagePacket(
6558 final Account account,
6559 final im.conversations.android.xmpp.model.stanza.Message packet) {
6560 final XmppConnection connection = account.getXmppConnection();
6561 if (connection != null) {
6562 connection.sendMessagePacket(packet);
6563 }
6564 }
6565
6566 public void sendPresencePacket(
6567 final Account account,
6568 final im.conversations.android.xmpp.model.stanza.Presence packet) {
6569 final XmppConnection connection = account.getXmppConnection();
6570 if (connection != null) {
6571 connection.sendPresencePacket(packet);
6572 }
6573 }
6574
6575 public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
6576 final XmppConnection connection = account.getXmppConnection();
6577 if (connection == null) {
6578 return;
6579 }
6580 connection.sendCreateAccountWithCaptchaPacket(id, data);
6581 }
6582
6583 public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback) {
6584 sendIqPacket(account, packet, callback, null);
6585 }
6586
6587 public void sendIqPacket(final Account account, final Iq packet, final Consumer<Iq> callback, Long timeout) {
6588 final XmppConnection connection = account.getXmppConnection();
6589 if (connection != null) {
6590 connection.sendIqPacket(packet, callback, timeout);
6591 } else if (callback != null) {
6592 callback.accept(Iq.TIMEOUT);
6593 }
6594 }
6595
6596 public void sendPresence(final Account account) {
6597 sendPresence(account, checkListeners() && broadcastLastActivity());
6598 }
6599
6600 private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
6601 final Presence.Status status;
6602 if (manuallyChangePresence()) {
6603 status = account.getPresenceStatus();
6604 } else {
6605 status = getTargetPresence();
6606 }
6607 final var packet = mPresenceGenerator.selfPresence(account, status);
6608 if (mLastActivity > 0 && includeIdleTimestamp) {
6609 long since =
6610 Math.min(mLastActivity, System.currentTimeMillis()); // don't send future dates
6611 packet.addChild("idle", Namespace.IDLE)
6612 .setAttribute("since", AbstractGenerator.getTimestamp(since));
6613 }
6614 sendPresencePacket(account, packet);
6615 }
6616
6617 private void deactivateGracePeriod() {
6618 for (Account account : getAccounts()) {
6619 account.deactivateGracePeriod();
6620 }
6621 }
6622
6623 public void refreshAllPresences() {
6624 boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
6625 for (Account account : getAccounts()) {
6626 if (account.isConnectionEnabled()) {
6627 sendPresence(account, includeIdleTimestamp);
6628 }
6629 }
6630 }
6631
6632 private void refreshAllFcmTokens() {
6633 for (Account account : getAccounts()) {
6634 if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
6635 mPushManagementService.registerPushTokenOnServer(account);
6636 }
6637 }
6638 }
6639
6640 private void sendOfflinePresence(final Account account) {
6641 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence");
6642 sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
6643 }
6644
6645 public MessageGenerator getMessageGenerator() {
6646 return this.mMessageGenerator;
6647 }
6648
6649 public PresenceGenerator getPresenceGenerator() {
6650 return this.mPresenceGenerator;
6651 }
6652
6653 public IqGenerator getIqGenerator() {
6654 return this.mIqGenerator;
6655 }
6656
6657 public JingleConnectionManager getJingleConnectionManager() {
6658 return this.mJingleConnectionManager;
6659 }
6660
6661 private boolean hasJingleRtpConnection(final Account account) {
6662 return this.mJingleConnectionManager.hasJingleRtpConnection(account);
6663 }
6664
6665 public MessageArchiveService getMessageArchiveService() {
6666 return this.mMessageArchiveService;
6667 }
6668
6669 public QuickConversationsService getQuickConversationsService() {
6670 return this.mQuickConversationsService;
6671 }
6672
6673 public List<Contact> findContacts(Jid jid, String accountJid) {
6674 ArrayList<Contact> contacts = new ArrayList<>();
6675 for (Account account : getAccounts()) {
6676 if ((account.isEnabled() || accountJid != null)
6677 && (accountJid == null
6678 || accountJid.equals(account.getJid().asBareJid().toString()))) {
6679 Contact contact = account.getRoster().getContactFromContactList(jid);
6680 if (contact != null) {
6681 contacts.add(contact);
6682 }
6683 }
6684 }
6685 return contacts;
6686 }
6687
6688 public Conversation findFirstMuc(Jid jid) {
6689 return findFirstMuc(jid, null);
6690 }
6691
6692 public Conversation findFirstMuc(Jid jid, String accountJid) {
6693 for (Conversation conversation : getConversations()) {
6694 if ((conversation.getAccount().isEnabled() || accountJid != null)
6695 && (accountJid == null || accountJid.equals(conversation.getAccount().getJid().asBareJid().toString()))
6696 && conversation.getJid().asBareJid().equals(jid.asBareJid()) && conversation.getMode() == Conversation.MODE_MULTI) {
6697 return conversation;
6698 }
6699 }
6700 return null;
6701 }
6702
6703 public NotificationService getNotificationService() {
6704 return this.mNotificationService;
6705 }
6706
6707 public HttpConnectionManager getHttpConnectionManager() {
6708 return this.mHttpConnectionManager;
6709 }
6710
6711 public void resendFailedMessages(final Message message) {
6712 message.setTime(System.currentTimeMillis());
6713 markMessage(message, Message.STATUS_WAITING);
6714 this.resendMessage(message, false);
6715 if (message.getConversation() instanceof Conversation c) {
6716 c.sort();
6717 }
6718 updateConversationUi();
6719 }
6720
6721 public void clearConversationHistory(final Conversation conversation) {
6722 final long clearDate;
6723 final String reference;
6724 if (conversation.countMessages() > 0) {
6725 Message latestMessage = conversation.getLatestMessage();
6726 clearDate = latestMessage.getTimeSent() + 1000;
6727 reference = latestMessage.getServerMsgId();
6728 } else {
6729 clearDate = System.currentTimeMillis();
6730 reference = null;
6731 }
6732 conversation.clearMessages();
6733 conversation.setHasMessagesLeftOnServer(false); // avoid messages getting loaded through mam
6734 conversation.setLastClearHistory(clearDate, reference);
6735 Runnable runnable =
6736 () -> {
6737 databaseBackend.deleteMessagesInConversation(conversation);
6738 databaseBackend.updateConversation(conversation);
6739 };
6740 mDatabaseWriterExecutor.execute(runnable);
6741 }
6742
6743 public boolean sendBlockRequest(
6744 final Blockable blockable, final boolean reportSpam, final String serverMsgId) {
6745 if (blockable != null && blockable.getBlockedJid() != null) {
6746 final var account = blockable.getAccount();
6747 final Jid jid = blockable.getBlockedJid();
6748 this.sendIqPacket(
6749 account,
6750 getIqGenerator().generateSetBlockRequest(jid, reportSpam, serverMsgId),
6751 (response) -> {
6752 if (response.getType() == Iq.Type.RESULT) {
6753 account.getBlocklist().add(jid);
6754 updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
6755 }
6756 });
6757 if (blockable.getBlockedJid().isFullJid()) {
6758 return false;
6759 } else if (removeBlockedConversations(blockable.getAccount(), jid)) {
6760 updateConversationUi();
6761 return true;
6762 } else {
6763 return false;
6764 }
6765 } else {
6766 return false;
6767 }
6768 }
6769
6770 public boolean removeBlockedConversations(final Account account, final Jid blockedJid) {
6771 boolean removed = false;
6772 synchronized (this.conversations) {
6773 boolean domainJid = blockedJid.getLocal() == null;
6774 for (Conversation conversation : this.conversations) {
6775 boolean jidMatches =
6776 (domainJid
6777 && blockedJid
6778 .getDomain()
6779 .equals(conversation.getJid().getDomain()))
6780 || blockedJid.equals(conversation.getJid().asBareJid());
6781 if (conversation.getAccount() == account
6782 && conversation.getMode() == Conversation.MODE_SINGLE
6783 && jidMatches) {
6784 this.conversations.remove(conversation);
6785 markRead(conversation);
6786 conversation.setStatus(Conversation.STATUS_ARCHIVED);
6787 Log.d(
6788 Config.LOGTAG,
6789 account.getJid().asBareJid()
6790 + ": archiving conversation "
6791 + conversation.getJid().asBareJid()
6792 + " because jid was blocked");
6793 updateConversation(conversation);
6794 removed = true;
6795 }
6796 }
6797 }
6798 return removed;
6799 }
6800
6801 public void sendUnblockRequest(final Blockable blockable) {
6802 if (blockable != null && blockable.getJid() != null) {
6803 final var account = blockable.getAccount();
6804 final Jid jid = blockable.getBlockedJid();
6805 this.sendIqPacket(
6806 account,
6807 getIqGenerator().generateSetUnblockRequest(jid),
6808 response -> {
6809 if (response.getType() == Iq.Type.RESULT) {
6810 account.getBlocklist().remove(jid);
6811 updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
6812 }
6813 });
6814 }
6815 }
6816
6817 public void publishDisplayName(final Account account) {
6818 String displayName = account.getDisplayName();
6819 final Iq request;
6820 if (TextUtils.isEmpty(displayName)) {
6821 request = mIqGenerator.deleteNode(Namespace.NICK);
6822 } else {
6823 request = mIqGenerator.publishNick(displayName);
6824 }
6825 mAvatarService.clear(account);
6826 sendIqPacket(
6827 account,
6828 request,
6829 (packet) -> {
6830 if (packet.getType() == Iq.Type.ERROR) {
6831 Log.d(
6832 Config.LOGTAG,
6833 account.getJid().asBareJid()
6834 + ": unable to modify nick name "
6835 + packet);
6836 }
6837 });
6838 }
6839
6840 public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String, String> key) {
6841 ServiceDiscoveryResult result = discoCache.get(key);
6842 if (result != null) {
6843 return result;
6844 } else {
6845 if (key.first == null || key.second == null) return null;
6846 result = databaseBackend.findDiscoveryResult(key.first, key.second);
6847 if (result != null) {
6848 discoCache.put(key, result);
6849 }
6850 return result;
6851 }
6852 }
6853
6854 public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) {
6855 final var request = new Iq(input == null ? Iq.Type.GET : Iq.Type.SET);
6856 request.setTo(jid);
6857 Element query = request.query("jabber:iq:gateway");
6858 if (input != null) {
6859 Element prompt = query.addChild("prompt");
6860 prompt.setContent(input);
6861 }
6862 sendIqPacket(account, request, packet -> {
6863 if (packet.getType() == Iq.Type.RESULT) {
6864 callback.onGatewayResult(packet.query().findChildContent(input == null ? "prompt" : "jid"), null);
6865 } else {
6866 Element error = packet.findChild("error");
6867 callback.onGatewayResult(null, error == null ? null : error.findChildContent("text"));
6868 }
6869 });
6870 }
6871
6872 public void fetchCaps(Account account, final Jid jid, final Presence presence) {
6873 fetchCaps(account, jid, presence, null);
6874 }
6875
6876 public void fetchCaps(Account account, final Jid jid, final Presence presence, Runnable cb) {
6877 final Pair<String, String> key = presence == null ? null : new Pair<>(presence.getHash(), presence.getVer());
6878 final ServiceDiscoveryResult disco = key == null ? null : getCachedServiceDiscoveryResult(key);
6879
6880 if (disco != null) {
6881 presence.setServiceDiscoveryResult(disco);
6882 final Contact contact = account.getRoster().getContact(jid);
6883 if (contact.refreshRtpCapability()) {
6884 syncRoster(account);
6885 }
6886 contact.refreshCaps();
6887 if (disco.hasIdentity("gateway", "pstn")) {
6888 contact.registerAsPhoneAccount(this);
6889 mQuickConversationsService.considerSyncBackground(false);
6890 }
6891 updateConversationUi(true);
6892 } else {
6893 final Iq request = new Iq(Iq.Type.GET);
6894 request.setTo(jid);
6895 final String node = presence == null ? null : presence.getNode();
6896 final String ver = presence == null ? null : presence.getVer();
6897 final Element query = request.query(Namespace.DISCO_INFO);
6898 if (node != null && ver != null) {
6899 query.setAttribute("node", node + "#" + ver);
6900 }
6901
6902 Log.d(
6903 Config.LOGTAG,
6904 account.getJid().asBareJid()
6905 + ": making disco request for "
6906 + (key == null ? null : key.second)
6907 + " to "
6908 + jid);
6909 sendIqPacket(
6910 account,
6911 request,
6912 (response) -> {
6913 if (response.getType() == Iq.Type.RESULT) {
6914 final ServiceDiscoveryResult discoveryResult =
6915 new ServiceDiscoveryResult(response);
6916 if (presence == null || presence.getVer() == null || presence.getVer().equals(discoveryResult.getVer())) {
6917 databaseBackend.insertDiscoveryResult(discoveryResult);
6918 injectServiceDiscoveryResult(
6919 account.getRoster(),
6920 presence == null ? null : presence.getHash(),
6921 presence == null ? null : presence.getVer(),
6922 jid.getResource(),
6923 discoveryResult);
6924 if (discoveryResult.hasIdentity("gateway", "pstn")) {
6925 final Contact contact = account.getRoster().getContact(jid);
6926 contact.registerAsPhoneAccount(this);
6927 mQuickConversationsService.considerSyncBackground(false);
6928 }
6929 updateConversationUi(true);
6930 if (cb != null) cb.run();
6931 } else {
6932 Log.d(
6933 Config.LOGTAG,
6934 account.getJid().asBareJid()
6935 + ": mismatch in caps for contact "
6936 + jid
6937 + " "
6938 + presence.getVer()
6939 + " vs "
6940 + discoveryResult.getVer());
6941 }
6942 } else {
6943 Log.d(
6944 Config.LOGTAG,
6945 account.getJid().asBareJid()
6946 + ": unable to fetch caps from "
6947 + jid);
6948 }
6949 });
6950 }
6951 }
6952
6953 public void fetchCommands(Account account, final Jid jid, Consumer<Iq> callback) {
6954 final var request = mIqGenerator.queryDiscoItems(jid, "http://jabber.org/protocol/commands");
6955 sendIqPacket(account, request, callback);
6956 }
6957
6958 private void injectServiceDiscoveryResult(
6959 Roster roster, String hash, String ver, String resource, ServiceDiscoveryResult disco) {
6960 boolean rosterNeedsSync = false;
6961 for (final Contact contact : roster.getContacts()) {
6962 boolean serviceDiscoverySet = false;
6963 Presence onePresence = contact.getPresences().get(resource == null ? "" : resource);
6964 if (onePresence != null) {
6965 onePresence.setServiceDiscoveryResult(disco);
6966 serviceDiscoverySet = true;
6967 } else if (resource == null && hash == null && ver == null) {
6968 Presence p = new Presence(Presence.Status.OFFLINE, null, null, null, "");
6969 p.setServiceDiscoveryResult(disco);
6970 contact.updatePresence("", p);
6971 serviceDiscoverySet = true;
6972 }
6973 if (hash != null && ver != null) {
6974 for (final Presence presence : contact.getPresences().getPresences()) {
6975 if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
6976 presence.setServiceDiscoveryResult(disco);
6977 serviceDiscoverySet = true;
6978 }
6979 }
6980 }
6981 if (serviceDiscoverySet) {
6982 rosterNeedsSync |= contact.refreshRtpCapability();
6983 contact.refreshCaps();
6984 }
6985 }
6986 if (rosterNeedsSync) {
6987 syncRoster(roster.getAccount());
6988 }
6989 }
6990
6991 public void fetchMamPreferences(final Account account, final OnMamPreferencesFetched callback) {
6992 final MessageArchiveService.Version version = MessageArchiveService.Version.get(account);
6993 final Iq request = new Iq(Iq.Type.GET);
6994 request.addChild("prefs", version.namespace);
6995 sendIqPacket(
6996 account,
6997 request,
6998 (packet) -> {
6999 final Element prefs = packet.findChild("prefs", version.namespace);
7000 if (packet.getType() == Iq.Type.RESULT && prefs != null) {
7001 callback.onPreferencesFetched(prefs);
7002 } else {
7003 callback.onPreferencesFetchFailed();
7004 }
7005 });
7006 }
7007
7008 public PushManagementService getPushManagementService() {
7009 return mPushManagementService;
7010 }
7011
7012 public void changeStatus(Account account, PresenceTemplate template, String signature) {
7013 if (!template.getStatusMessage().isEmpty()) {
7014 databaseBackend.insertPresenceTemplate(template);
7015 }
7016 account.setPgpSignature(signature);
7017 account.setPresenceStatus(template.getStatus());
7018 account.setPresenceStatusMessage(template.getStatusMessage());
7019 databaseBackend.updateAccount(account);
7020 sendPresence(account);
7021 }
7022
7023 public List<PresenceTemplate> getPresenceTemplates(Account account) {
7024 List<PresenceTemplate> templates = databaseBackend.getPresenceTemplates();
7025 for (PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) {
7026 if (!templates.contains(template)) {
7027 templates.add(0, template);
7028 }
7029 }
7030 return templates;
7031 }
7032
7033 public void saveConversationAsBookmark(final Conversation conversation, final String name) {
7034 final Account account = conversation.getAccount();
7035 final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid());
7036 String nick = conversation.getMucOptions().getActualNick();
7037 if (nick == null) nick = conversation.getJid().getResource();
7038 if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
7039 bookmark.setNick(nick);
7040 }
7041 if (!TextUtils.isEmpty(name)) {
7042 bookmark.setBookmarkName(name);
7043 }
7044 bookmark.setAutojoin(true);
7045 createBookmark(account, bookmark);
7046 bookmark.setConversation(conversation);
7047 }
7048
7049 public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
7050 boolean performedVerification = false;
7051 final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
7052 for (XmppUri.Fingerprint fp : fingerprints) {
7053 if (fp.type == XmppUri.FingerprintType.OMEMO) {
7054 String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
7055 FingerprintStatus fingerprintStatus =
7056 axolotlService.getFingerprintTrust(fingerprint);
7057 if (fingerprintStatus != null) {
7058 if (!fingerprintStatus.isVerified()) {
7059 performedVerification = true;
7060 axolotlService.setFingerprintTrust(
7061 fingerprint, fingerprintStatus.toVerified());
7062 }
7063 } else {
7064 axolotlService.preVerifyFingerprint(contact, fingerprint);
7065 }
7066 }
7067 }
7068 return performedVerification;
7069 }
7070
7071 public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
7072 final AxolotlService axolotlService = account.getAxolotlService();
7073 boolean verifiedSomething = false;
7074 for (XmppUri.Fingerprint fp : fingerprints) {
7075 if (fp.type == XmppUri.FingerprintType.OMEMO) {
7076 String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
7077 Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint);
7078 FingerprintStatus fingerprintStatus =
7079 axolotlService.getFingerprintTrust(fingerprint);
7080 if (fingerprintStatus != null) {
7081 if (!fingerprintStatus.isVerified()) {
7082 axolotlService.setFingerprintTrust(
7083 fingerprint, fingerprintStatus.toVerified());
7084 verifiedSomething = true;
7085 }
7086 } else {
7087 axolotlService.preVerifyFingerprint(account, fingerprint);
7088 verifiedSomething = true;
7089 }
7090 }
7091 }
7092 return verifiedSomething;
7093 }
7094
7095 public boolean blindTrustBeforeVerification() {
7096 return getBooleanPreference(AppSettings.BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv);
7097 }
7098
7099 public ShortcutService getShortcutService() {
7100 return mShortcutService;
7101 }
7102
7103 public void pushMamPreferences(Account account, Element prefs) {
7104 final Iq set = new Iq(Iq.Type.SET);
7105 set.addChild(prefs);
7106 account.setMamPrefs(prefs);
7107 sendIqPacket(account, set, null);
7108 }
7109
7110 public void evictPreview(File f) {
7111 if (f == null) return;
7112
7113 if (mDrawableCache.remove(f.getAbsolutePath()) != null) {
7114 Log.d(Config.LOGTAG, "deleted cached preview");
7115 }
7116 }
7117
7118 public void evictPreview(String uuid) {
7119 if (mDrawableCache.remove(uuid) != null) {
7120 Log.d(Config.LOGTAG, "deleted cached preview");
7121 }
7122 }
7123
7124 public interface OnMamPreferencesFetched {
7125 void onPreferencesFetched(Element prefs);
7126
7127 void onPreferencesFetchFailed();
7128 }
7129
7130 public interface OnAccountCreated {
7131 void onAccountCreated(Account account);
7132
7133 void informUser(int r);
7134 }
7135
7136 public interface OnMoreMessagesLoaded {
7137 void onMoreMessagesLoaded(int count, Conversation conversation);
7138
7139 void informUser(int r);
7140 }
7141
7142 public interface OnAccountPasswordChanged {
7143 void onPasswordChangeSucceeded();
7144
7145 void onPasswordChangeFailed();
7146 }
7147
7148 public interface OnRoomDestroy {
7149 void onRoomDestroySucceeded();
7150
7151 void onRoomDestroyFailed();
7152 }
7153
7154 public interface OnAffiliationChanged {
7155 void onAffiliationChangedSuccessful(Jid jid);
7156
7157 void onAffiliationChangeFailed(Jid jid, int resId);
7158 }
7159
7160 public interface OnConversationUpdate {
7161 default void onConversationUpdate() { onConversationUpdate(false); }
7162 default void onConversationUpdate(boolean newCaps) { onConversationUpdate(); }
7163 }
7164
7165 public interface OnJingleRtpConnectionUpdate {
7166 void onJingleRtpConnectionUpdate(
7167 final Account account,
7168 final Jid with,
7169 final String sessionId,
7170 final RtpEndUserState state);
7171
7172 void onAudioDeviceChanged(
7173 CallIntegration.AudioDevice selectedAudioDevice,
7174 Set<CallIntegration.AudioDevice> availableAudioDevices);
7175 }
7176
7177 public interface OnAccountUpdate {
7178 void onAccountUpdate();
7179 }
7180
7181 public interface OnCaptchaRequested {
7182 void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha);
7183 }
7184
7185 public interface OnRosterUpdate {
7186 void onRosterUpdate(final UpdateRosterReason reason, final Contact contact);
7187 }
7188
7189 public interface OnMucRosterUpdate {
7190 void onMucRosterUpdate();
7191 }
7192
7193 public interface OnConferenceConfigurationFetched {
7194 void onConferenceConfigurationFetched(Conversation conversation);
7195
7196 void onFetchFailed(Conversation conversation, String errorCondition);
7197 }
7198
7199 public interface OnConferenceJoined {
7200 void onConferenceJoined(Conversation conversation);
7201 }
7202
7203 public interface OnConfigurationPushed {
7204 void onPushSucceeded();
7205
7206 void onPushFailed();
7207 }
7208
7209 public interface OnShowErrorToast {
7210 void onShowErrorToast(int resId);
7211 }
7212
7213 public class XmppConnectionBinder extends Binder {
7214 public XmppConnectionService getService() {
7215 return XmppConnectionService.this;
7216 }
7217 }
7218
7219 private class InternalEventReceiver extends BroadcastReceiver {
7220
7221 @Override
7222 public void onReceive(final Context context, final Intent intent) {
7223 onStartCommand(intent, 0, 0);
7224 }
7225 }
7226
7227 private class RestrictedEventReceiver extends BroadcastReceiver {
7228
7229 private final Collection<String> allowedActions;
7230
7231 private RestrictedEventReceiver(final Collection<String> allowedActions) {
7232 this.allowedActions = allowedActions;
7233 }
7234
7235 @Override
7236 public void onReceive(final Context context, final Intent intent) {
7237 final String action = intent == null ? null : intent.getAction();
7238 if (allowedActions.contains(action)) {
7239 onStartCommand(intent, 0, 0);
7240 } else {
7241 Log.e(Config.LOGTAG, "restricting broadcast of event " + action);
7242 }
7243 }
7244 }
7245
7246 public static class OngoingCall {
7247 public final AbstractJingleConnection.Id id;
7248 public final Set<Media> media;
7249 public final boolean reconnecting;
7250
7251 public OngoingCall(
7252 AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
7253 this.id = id;
7254 this.media = media;
7255 this.reconnecting = reconnecting;
7256 }
7257
7258 @Override
7259 public boolean equals(Object o) {
7260 if (this == o) return true;
7261 if (o == null || getClass() != o.getClass()) return false;
7262 OngoingCall that = (OngoingCall) o;
7263 return reconnecting == that.reconnecting
7264 && Objects.equal(id, that.id)
7265 && Objects.equal(media, that.media);
7266 }
7267
7268 @Override
7269 public int hashCode() {
7270 return Objects.hashCode(id, media, reconnecting);
7271 }
7272 }
7273
7274 public static void toggleForegroundService(final XmppConnectionService service) {
7275 if (service == null) {
7276 return;
7277 }
7278 service.toggleForegroundService();
7279 }
7280
7281 public static void toggleForegroundService(final ConversationsActivity activity) {
7282 if (activity == null) {
7283 return;
7284 }
7285 toggleForegroundService(activity.xmppConnectionService);
7286 }
7287
7288 public static class BlockedMediaException extends Exception { }
7289
7290 public static enum UpdateRosterReason {
7291 INIT,
7292 AVATAR,
7293 PUSH,
7294 PRESENCE
7295 }
7296}