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