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