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.annotation.TargetApi;
9import android.app.AlarmManager;
10import android.app.KeyguardManager;
11import android.app.Notification;
12import android.app.NotificationManager;
13import android.app.PendingIntent;
14import android.app.Service;
15import android.content.BroadcastReceiver;
16import android.content.ComponentName;
17import android.content.Context;
18import android.content.Intent;
19import android.content.IntentFilter;
20import android.content.SharedPreferences;
21import android.content.pm.PackageManager;
22import android.database.ContentObserver;
23import android.graphics.Bitmap;
24import android.graphics.drawable.BitmapDrawable;
25import android.graphics.drawable.Drawable;
26import android.media.AudioManager;
27import android.net.ConnectivityManager;
28import android.net.Network;
29import android.net.NetworkCapabilities;
30import android.net.NetworkInfo;
31import android.net.Uri;
32import android.os.Binder;
33import android.os.Build;
34import android.os.Bundle;
35import android.os.Environment;
36import android.os.IBinder;
37import android.os.PowerManager;
38import android.os.PowerManager.WakeLock;
39import android.os.SystemClock;
40import android.preference.PreferenceManager;
41import android.provider.ContactsContract;
42import android.security.KeyChain;
43import android.telephony.PhoneStateListener;
44import android.telephony.TelephonyManager;
45import android.text.TextUtils;
46import android.util.DisplayMetrics;
47import android.util.Log;
48import android.util.LruCache;
49import android.util.Pair;
50
51import androidx.annotation.BoolRes;
52import androidx.annotation.IntegerRes;
53import androidx.annotation.NonNull;
54import androidx.core.app.RemoteInput;
55import androidx.core.content.ContextCompat;
56
57import com.google.common.base.Objects;
58import com.google.common.base.Optional;
59import com.google.common.base.Strings;
60
61import org.conscrypt.Conscrypt;
62import org.openintents.openpgp.IOpenPgpService2;
63import org.openintents.openpgp.util.OpenPgpApi;
64import org.openintents.openpgp.util.OpenPgpServiceConnection;
65
66import java.io.File;
67import java.io.FileInputStream;
68import java.io.IOException;
69import java.security.Security;
70import java.security.cert.CertificateException;
71import java.security.cert.X509Certificate;
72import java.text.ParseException;
73import java.util.ArrayList;
74import java.util.Arrays;
75import java.util.Collection;
76import java.util.Collections;
77import java.util.HashSet;
78import java.util.Hashtable;
79import java.util.Iterator;
80import java.util.List;
81import java.util.ListIterator;
82import java.util.Map;
83import java.util.Set;
84import java.util.WeakHashMap;
85import java.util.concurrent.CopyOnWriteArrayList;
86import java.util.concurrent.CountDownLatch;
87import java.util.concurrent.Executor;
88import java.util.concurrent.Executors;
89import java.util.concurrent.atomic.AtomicBoolean;
90import java.util.concurrent.atomic.AtomicLong;
91import java.util.concurrent.atomic.AtomicReference;
92
93import io.ipfs.cid.Cid;
94
95import eu.siacs.conversations.Config;
96import eu.siacs.conversations.R;
97import eu.siacs.conversations.android.JabberIdContact;
98import eu.siacs.conversations.crypto.OmemoSetting;
99import eu.siacs.conversations.crypto.PgpDecryptionService;
100import eu.siacs.conversations.crypto.PgpEngine;
101import eu.siacs.conversations.crypto.axolotl.AxolotlService;
102import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
103import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
104import eu.siacs.conversations.entities.Account;
105import eu.siacs.conversations.entities.Blockable;
106import eu.siacs.conversations.entities.Bookmark;
107import eu.siacs.conversations.entities.Contact;
108import eu.siacs.conversations.entities.Conversation;
109import eu.siacs.conversations.entities.Conversational;
110import eu.siacs.conversations.entities.DownloadableFile;
111import eu.siacs.conversations.entities.Message;
112import eu.siacs.conversations.entities.MucOptions;
113import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
114import eu.siacs.conversations.entities.Presence;
115import eu.siacs.conversations.entities.PresenceTemplate;
116import eu.siacs.conversations.entities.Roster;
117import eu.siacs.conversations.entities.ServiceDiscoveryResult;
118import eu.siacs.conversations.generator.AbstractGenerator;
119import eu.siacs.conversations.generator.IqGenerator;
120import eu.siacs.conversations.generator.MessageGenerator;
121import eu.siacs.conversations.generator.PresenceGenerator;
122import eu.siacs.conversations.http.HttpConnectionManager;
123import eu.siacs.conversations.parser.AbstractParser;
124import eu.siacs.conversations.parser.IqParser;
125import eu.siacs.conversations.parser.MessageParser;
126import eu.siacs.conversations.parser.PresenceParser;
127import eu.siacs.conversations.persistance.DatabaseBackend;
128import eu.siacs.conversations.persistance.FileBackend;
129import eu.siacs.conversations.persistance.UnifiedPushDatabase;
130import eu.siacs.conversations.ui.ChooseAccountForProfilePictureActivity;
131import eu.siacs.conversations.ui.RtpSessionActivity;
132import eu.siacs.conversations.ui.SettingsActivity;
133import eu.siacs.conversations.ui.UiCallback;
134import eu.siacs.conversations.ui.interfaces.OnAvatarPublication;
135import eu.siacs.conversations.ui.interfaces.OnMediaLoaded;
136import eu.siacs.conversations.ui.interfaces.OnSearchResultsAvailable;
137import eu.siacs.conversations.utils.AccountUtils;
138import eu.siacs.conversations.utils.Compatibility;
139import eu.siacs.conversations.utils.ConversationsFileObserver;
140import eu.siacs.conversations.utils.CryptoHelper;
141import eu.siacs.conversations.utils.EasyOnboardingInvite;
142import eu.siacs.conversations.utils.ExceptionHelper;
143import eu.siacs.conversations.utils.MimeUtils;
144import eu.siacs.conversations.utils.PhoneHelper;
145import eu.siacs.conversations.utils.QuickLoader;
146import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
147import eu.siacs.conversations.utils.ReplacingTaskManager;
148import eu.siacs.conversations.utils.Resolver;
149import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
150import eu.siacs.conversations.utils.StringUtils;
151import eu.siacs.conversations.utils.TorServiceUtils;
152import eu.siacs.conversations.utils.ThemeHelper;
153import eu.siacs.conversations.utils.WakeLockHelper;
154import eu.siacs.conversations.utils.XmppUri;
155import eu.siacs.conversations.xml.Element;
156import eu.siacs.conversations.xml.LocalizedContent;
157import eu.siacs.conversations.xml.Namespace;
158import eu.siacs.conversations.xmpp.Jid;
159import eu.siacs.conversations.xmpp.OnBindListener;
160import eu.siacs.conversations.xmpp.OnContactStatusChanged;
161import eu.siacs.conversations.xmpp.OnGatewayResult;
162import eu.siacs.conversations.xmpp.OnIqPacketReceived;
163import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
164import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
165import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
166import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
167import eu.siacs.conversations.xmpp.OnStatusChanged;
168import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
169import eu.siacs.conversations.xmpp.XmppConnection;
170import eu.siacs.conversations.xmpp.chatstate.ChatState;
171import eu.siacs.conversations.xmpp.forms.Data;
172import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
173import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
174import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
175import eu.siacs.conversations.xmpp.jingle.Media;
176import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
177import eu.siacs.conversations.xmpp.mam.MamReference;
178import eu.siacs.conversations.xmpp.pep.Avatar;
179import eu.siacs.conversations.xmpp.pep.PublishOptions;
180import eu.siacs.conversations.xmpp.stanzas.IqPacket;
181import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
182import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
183import me.leolin.shortcutbadger.ShortcutBadger;
184
185public class XmppConnectionService extends Service {
186
187 public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations";
188 public static final String ACTION_MARK_AS_READ = "mark_as_read";
189 public static final String ACTION_SNOOZE = "snooze";
190 public static final String ACTION_CLEAR_MESSAGE_NOTIFICATION = "clear_message_notification";
191 public static final String ACTION_CLEAR_MISSED_CALL_NOTIFICATION = "clear_missed_call_notification";
192 public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error";
193 public static final String ACTION_TRY_AGAIN = "try_again";
194 public static final String ACTION_IDLE_PING = "idle_ping";
195 public static final String ACTION_FCM_TOKEN_REFRESH = "fcm_token_refresh";
196 public static final String ACTION_FCM_MESSAGE_RECEIVED = "fcm_message_received";
197 public static final String ACTION_DISMISS_CALL = "dismiss_call";
198 public static final String ACTION_END_CALL = "end_call";
199 public static final String ACTION_PROVISION_ACCOUNT = "provision_account";
200 private static final String ACTION_POST_CONNECTIVITY_CHANGE = "eu.siacs.conversations.POST_CONNECTIVITY_CHANGE";
201 public static final String ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS = "eu.siacs.conversations.UNIFIED_PUSH_RENEW";
202
203 private static final String SETTING_LAST_ACTIVITY_TS = "last_activity_timestamp";
204
205 public final CountDownLatch restoredFromDatabaseLatch = new CountDownLatch(1);
206 private final static Executor FILE_OBSERVER_EXECUTOR = Executors.newSingleThreadExecutor();
207 private final static Executor FILE_ATTACHMENT_EXECUTOR = Executors.newSingleThreadExecutor();
208 private final static SerialSingleThreadExecutor VIDEO_COMPRESSION_EXECUTOR = new SerialSingleThreadExecutor("VideoCompression");
209 private final SerialSingleThreadExecutor mDatabaseWriterExecutor = new SerialSingleThreadExecutor("DatabaseWriter");
210 private final SerialSingleThreadExecutor mDatabaseReaderExecutor = new SerialSingleThreadExecutor("DatabaseReader");
211 private final SerialSingleThreadExecutor mNotificationExecutor = new SerialSingleThreadExecutor("NotificationExecutor");
212 private final ReplacingTaskManager mRosterSyncTaskManager = new ReplacingTaskManager();
213 private final IBinder mBinder = new XmppConnectionBinder();
214 private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
215 private final IqGenerator mIqGenerator = new IqGenerator(this);
216 private final Set<String> mInProgressAvatarFetches = new HashSet<>();
217 private final Set<String> mOmittedPepAvatarFetches = new HashSet<>();
218 private final HashSet<Jid> mLowPingTimeoutMode = new HashSet<>();
219 private final OnIqPacketReceived mDefaultIqHandler = (account, packet) -> {
220 if (packet.getType() != IqPacket.TYPE.RESULT) {
221 Element error = packet.findChild("error");
222 String text = error != null ? error.findChildContent("text") : null;
223 if (text != null) {
224 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received iq error - " + text);
225 }
226 }
227 };
228 public DatabaseBackend databaseBackend;
229 private final ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor("ContactMerger");
230 private long mLastActivity = 0;
231 private final FileBackend fileBackend = new FileBackend(this);
232 private MemorizingTrustManager mMemorizingTrustManager;
233 private final NotificationService mNotificationService = new NotificationService(this);
234 private final UnifiedPushBroker unifiedPushBroker = new UnifiedPushBroker(this);
235 private final ChannelDiscoveryService mChannelDiscoveryService = new ChannelDiscoveryService(this);
236 private final ShortcutService mShortcutService = new ShortcutService(this);
237 private final AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false);
238 private final AtomicBoolean mForceForegroundService = new AtomicBoolean(false);
239 private final AtomicBoolean mForceDuringOnCreate = new AtomicBoolean(false);
240 private final AtomicReference<OngoingCall> ongoingCall = new AtomicReference<>();
241 private final OnMessagePacketReceived mMessageParser = new MessageParser(this);
242 private final OnPresencePacketReceived mPresenceParser = new PresenceParser(this);
243 private final IqParser mIqParser = new IqParser(this);
244 private final MessageGenerator mMessageGenerator = new MessageGenerator(this);
245 public OnContactStatusChanged onContactStatusChanged = (contact, online) -> {
246 Conversation conversation = find(getConversations(), contact);
247 if (conversation != null) {
248 if (online) {
249 if (contact.getPresences().size() == 1) {
250 sendUnsentMessages(conversation);
251 }
252 }
253 }
254 };
255 private final PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
256 private List<Account> accounts;
257 private final JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(this);
258 private final HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(this);
259 private final AvatarService mAvatarService = new AvatarService(this);
260 private final MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
261 private final PushManagementService mPushManagementService = new PushManagementService(this);
262 private final QuickConversationsService mQuickConversationsService = new QuickConversationsService(this);
263 private final ConversationsFileObserver fileObserver = new ConversationsFileObserver(
264 Environment.getExternalStorageDirectory().getAbsolutePath()
265 ) {
266 @Override
267 public void onEvent(final int event, final File file) {
268 markFileDeleted(file);
269 }
270 };
271 private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
272
273 @Override
274 public boolean onMessageAcknowledged(final Account account, final Jid to, final String id) {
275 if (id.startsWith(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX)) {
276 final String sessionId = id.substring(JingleRtpConnection.JINGLE_MESSAGE_PROPOSE_ID_PREFIX.length());
277 mJingleConnectionManager.updateProposedSessionDiscovered(
278 account,
279 to,
280 sessionId,
281 JingleConnectionManager.DeviceDiscoveryState.SEARCHING_ACKNOWLEDGED
282 );
283 }
284
285
286 final Jid bare = to.asBareJid();
287
288 for (final Conversation conversation : getConversations()) {
289 if (conversation.getAccount() == account && conversation.getJid().asBareJid().equals(bare)) {
290 final Message message = conversation.findUnsentMessageWithUuid(id);
291 if (message != null) {
292 message.setStatus(Message.STATUS_SEND);
293 message.setErrorMessage(null);
294 databaseBackend.updateMessage(message, false);
295 return true;
296 }
297 }
298 }
299 return false;
300 }
301 };
302 private final AtomicBoolean isPhoneInCall = new AtomicBoolean(false);
303 private final AtomicBoolean diallerIntegrationActive = new AtomicBoolean(false);
304 private final PhoneStateListener phoneStateListener = new PhoneStateListener() {
305 @Override
306 public void onCallStateChanged(final int state, final String phoneNumber) {
307 if (diallerIntegrationActive.get()) return;
308 isPhoneInCall.set(state != TelephonyManager.CALL_STATE_IDLE);
309 if (state == TelephonyManager.CALL_STATE_OFFHOOK) {
310 mJingleConnectionManager.notifyPhoneCallStarted();
311 }
312 }
313 };
314
315 public void setDiallerIntegrationActive(boolean active) {
316 diallerIntegrationActive.set(active);
317 }
318
319 private boolean destroyed = false;
320
321 private int unreadCount = -1;
322
323 //Ui callback listeners
324 private final Set<OnConversationUpdate> mOnConversationUpdates = Collections.newSetFromMap(new WeakHashMap<OnConversationUpdate, Boolean>());
325 private final Set<OnShowErrorToast> mOnShowErrorToasts = Collections.newSetFromMap(new WeakHashMap<OnShowErrorToast, Boolean>());
326 private final Set<OnAccountUpdate> mOnAccountUpdates = Collections.newSetFromMap(new WeakHashMap<OnAccountUpdate, Boolean>());
327 private final Set<OnCaptchaRequested> mOnCaptchaRequested = Collections.newSetFromMap(new WeakHashMap<OnCaptchaRequested, Boolean>());
328 private final Set<OnRosterUpdate> mOnRosterUpdates = Collections.newSetFromMap(new WeakHashMap<OnRosterUpdate, Boolean>());
329 private final Set<OnUpdateBlocklist> mOnUpdateBlocklist = Collections.newSetFromMap(new WeakHashMap<OnUpdateBlocklist, Boolean>());
330 private final Set<OnMucRosterUpdate> mOnMucRosterUpdate = Collections.newSetFromMap(new WeakHashMap<OnMucRosterUpdate, Boolean>());
331 private final Set<OnKeyStatusUpdated> mOnKeyStatusUpdated = Collections.newSetFromMap(new WeakHashMap<OnKeyStatusUpdated, Boolean>());
332 private final Set<OnJingleRtpConnectionUpdate> onJingleRtpConnectionUpdate = Collections.newSetFromMap(new WeakHashMap<OnJingleRtpConnectionUpdate, Boolean>());
333
334 private final Object LISTENER_LOCK = new Object();
335
336
337 public final Set<String> FILENAMES_TO_IGNORE_DELETION = new HashSet<>();
338
339
340 private final OnBindListener mOnBindListener = new OnBindListener() {
341
342 @Override
343 public void onBind(final Account account) {
344 synchronized (mInProgressAvatarFetches) {
345 for (Iterator<String> iterator = mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) {
346 final String KEY = iterator.next();
347 if (KEY.startsWith(account.getJid().asBareJid() + "_")) {
348 iterator.remove();
349 }
350 }
351 }
352 boolean loggedInSuccessfully = account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY, true);
353 boolean gainedFeature = account.setOption(Account.OPTION_HTTP_UPLOAD_AVAILABLE, account.getXmppConnection().getFeatures().httpUpload(0));
354 if (loggedInSuccessfully || gainedFeature) {
355 databaseBackend.updateAccount(account);
356 }
357
358 if (loggedInSuccessfully) {
359 if (!TextUtils.isEmpty(account.getDisplayName())) {
360 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": display name wasn't empty on first log in. publishing");
361 publishDisplayName(account);
362 }
363 }
364
365 account.getRoster().clearPresences();
366 synchronized (account.inProgressConferenceJoins) {
367 account.inProgressConferenceJoins.clear();
368 }
369 synchronized (account.inProgressConferencePings) {
370 account.inProgressConferencePings.clear();
371 }
372 mJingleConnectionManager.notifyRebound(account);
373 mQuickConversationsService.considerSyncBackground(false);
374 fetchRosterFromServer(account);
375
376 final XmppConnection connection = account.getXmppConnection();
377
378 if (connection.getFeatures().bookmarks2()) {
379 fetchBookmarks2(account);
380 } else if (!account.getXmppConnection().getFeatures().bookmarksConversion()) {
381 fetchBookmarks(account);
382 }
383 final boolean flexible = account.getXmppConnection().getFeatures().flexibleOfflineMessageRetrieval();
384 final boolean catchup = getMessageArchiveService().inCatchup(account);
385 if (flexible && catchup && account.getXmppConnection().isMamPreferenceAlways()) {
386 sendIqPacket(account, mIqGenerator.purgeOfflineMessages(), (acc, packet) -> {
387 if (packet.getType() == IqPacket.TYPE.RESULT) {
388 Log.d(Config.LOGTAG, acc.getJid().asBareJid() + ": successfully purged offline messages");
389 }
390 });
391 }
392 sendPresence(account);
393 if (mPushManagementService.available(account)) {
394 mPushManagementService.registerPushTokenOnServer(account);
395 }
396 connectMultiModeConversations(account);
397 syncDirtyContacts(account);
398
399 unifiedPushBroker.renewUnifiedPushEndpointsOnBind(account);
400 }
401 };
402 private final AtomicLong mLastExpiryRun = new AtomicLong(0);
403 private final LruCache<Pair<String, String>, ServiceDiscoveryResult> discoCache = new LruCache<>(20);
404 private final OnStatusChanged statusListener = new OnStatusChanged() {
405
406 @Override
407 public void onStatusChanged(final Account account) {
408 XmppConnection connection = account.getXmppConnection();
409 updateAccountUi();
410
411 if (account.getStatus() == Account.State.ONLINE || account.getStatus().isError()) {
412 mQuickConversationsService.signalAccountStateChange();
413 }
414
415 if (account.getStatus() == Account.State.ONLINE) {
416 synchronized (mLowPingTimeoutMode) {
417 if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
418 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": leaving low ping timeout mode");
419 }
420 }
421 if (account.setShowErrorNotification(true)) {
422 databaseBackend.updateAccount(account);
423 }
424 mMessageArchiveService.executePendingQueries(account);
425 if (connection != null && connection.getFeatures().csi()) {
426 if (checkListeners()) {
427 Log.d(Config.LOGTAG, account.getJid().asBareJid() + " sending csi//inactive");
428 connection.sendInactive();
429 } else {
430 Log.d(Config.LOGTAG, account.getJid().asBareJid() + " sending csi//active");
431 connection.sendActive();
432 }
433 }
434 List<Conversation> conversations = getConversations();
435 for (Conversation conversation : conversations) {
436 final boolean inProgressJoin;
437 synchronized (account.inProgressConferenceJoins) {
438 inProgressJoin = account.inProgressConferenceJoins.contains(conversation);
439 }
440 final boolean pendingJoin;
441 synchronized (account.pendingConferenceJoins) {
442 pendingJoin = account.pendingConferenceJoins.contains(conversation);
443 }
444 if (conversation.getAccount() == account
445 && !pendingJoin
446 && !inProgressJoin) {
447 sendUnsentMessages(conversation);
448 }
449 }
450 final List<Conversation> pendingLeaves;
451 synchronized (account.pendingConferenceLeaves) {
452 pendingLeaves = new ArrayList<>(account.pendingConferenceLeaves);
453 account.pendingConferenceLeaves.clear();
454
455 }
456 for (Conversation conversation : pendingLeaves) {
457 leaveMuc(conversation);
458 }
459 final List<Conversation> pendingJoins;
460 synchronized (account.pendingConferenceJoins) {
461 pendingJoins = new ArrayList<>(account.pendingConferenceJoins);
462 account.pendingConferenceJoins.clear();
463 }
464 for (Conversation conversation : pendingJoins) {
465 joinMuc(conversation);
466 }
467 scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
468 } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) {
469 resetSendingToWaiting(account);
470 if (account.isEnabled() && isInLowPingTimeoutMode(account)) {
471 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": went into offline state during low ping mode. reconnecting now");
472 reconnectAccount(account, true, false);
473 } else {
474 final int timeToReconnect = SECURE_RANDOM.nextInt(10) + 2;
475 scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
476 }
477 } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
478 databaseBackend.updateAccount(account);
479 reconnectAccount(account, true, false);
480 } else if (account.getStatus() != Account.State.CONNECTING && account.getStatus() != Account.State.NO_INTERNET) {
481 resetSendingToWaiting(account);
482 if (connection != null && account.getStatus().isAttemptReconnect()) {
483 final int next = connection.getTimeToNextAttempt();
484 final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account);
485 if (next <= 0) {
486 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error connecting account. reconnecting now. lowPingTimeout=" + lowPingTimeoutMode);
487 reconnectAccount(account, true, false);
488 } else {
489 final int attempt = connection.getAttempt() + 1;
490 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error connecting account. try again in " + next + "s for the " + attempt + " time. lowPingTimeout=" + lowPingTimeoutMode);
491 scheduleWakeUpCall(next, account.getUuid().hashCode());
492 }
493 }
494 }
495 getNotificationService().updateErrorNotification();
496 }
497 };
498 private OpenPgpServiceConnection pgpServiceConnection;
499 private PgpEngine mPgpEngine = null;
500 private WakeLock wakeLock;
501 private LruCache<String, Bitmap> mBitmapCache;
502 private LruCache<String, Drawable> mDrawableCache;
503 private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
504 private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
505
506 private static String generateFetchKey(Account account, final Avatar avatar) {
507 return account.getJid().asBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum;
508 }
509
510 private boolean isInLowPingTimeoutMode(Account account) {
511 synchronized (mLowPingTimeoutMode) {
512 return mLowPingTimeoutMode.contains(account.getJid().asBareJid());
513 }
514 }
515
516 public void startForcingForegroundNotification() {
517 mForceForegroundService.set(true);
518 toggleForegroundService();
519 }
520
521 public void stopForcingForegroundNotification() {
522 mForceForegroundService.set(false);
523 toggleForegroundService();
524 }
525
526 public boolean areMessagesInitialized() {
527 return this.restoredFromDatabaseLatch.getCount() == 0;
528 }
529
530 public PgpEngine getPgpEngine() {
531 if (!Config.supportOpenPgp()) {
532 return null;
533 } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
534 if (this.mPgpEngine == null) {
535 this.mPgpEngine = new PgpEngine(new OpenPgpApi(
536 getApplicationContext(),
537 pgpServiceConnection.getService()), this);
538 }
539 return mPgpEngine;
540 } else {
541 return null;
542 }
543
544 }
545
546 public OpenPgpApi getOpenPgpApi() {
547 if (!Config.supportOpenPgp()) {
548 return null;
549 } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
550 return new OpenPgpApi(this, pgpServiceConnection.getService());
551 } else {
552 return null;
553 }
554 }
555
556 public FileBackend getFileBackend() {
557 return this.fileBackend;
558 }
559
560 public DownloadableFile getFileForCid(Cid cid) {
561 return this.databaseBackend.getFileForCid(cid);
562 }
563
564 public String getUrlForCid(Cid cid) {
565 return this.databaseBackend.getUrlForCid(cid);
566 }
567
568 public void saveCid(Cid cid, File file) throws BlockedMediaException {
569 saveCid(cid, file, null);
570 }
571
572 public void saveCid(Cid cid, File file, String url) throws BlockedMediaException {
573 if (this.databaseBackend.isBlockedMedia(cid)) {
574 throw new BlockedMediaException();
575 }
576 this.databaseBackend.saveCid(cid, file, url);
577 }
578
579 public void blockMedia(File f) {
580 try {
581 Cid[] cids = getFileBackend().calculateCids(new FileInputStream(f));
582 for (Cid cid : cids) {
583 blockMedia(cid);
584 }
585 } catch (final IOException e) { }
586 }
587
588 public void blockMedia(Cid cid) {
589 this.databaseBackend.blockMedia(cid);
590 }
591
592 public void clearBlockedMedia() {
593 this.databaseBackend.clearBlockedMedia();
594 }
595
596 public AvatarService getAvatarService() {
597 return this.mAvatarService;
598 }
599
600 public void attachLocationToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
601 int encryption = conversation.getNextEncryption();
602 if (encryption == Message.ENCRYPTION_PGP) {
603 encryption = Message.ENCRYPTION_DECRYPTED;
604 }
605 Message message = new Message(conversation, uri.toString(), encryption);
606 message.setThread(conversation.getThread());
607 Message.configurePrivateMessage(message);
608 if (encryption == Message.ENCRYPTION_DECRYPTED) {
609 getPgpEngine().encrypt(message, callback);
610 } else {
611 sendMessage(message);
612 callback.success(message);
613 }
614 }
615
616 public void attachFileToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback<Message> callback) {
617 final Message message;
618 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
619 message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
620 } else {
621 message = new Message(conversation, "", conversation.getNextEncryption());
622 }
623 message.setThread(conversation.getThread());
624 if (!Message.configurePrivateFileMessage(message)) {
625 message.setCounterpart(conversation.getNextCounterpart());
626 message.setType(Message.TYPE_FILE);
627 }
628 Log.d(Config.LOGTAG, "attachFile: type=" + message.getType());
629 Log.d(Config.LOGTAG, "counterpart=" + message.getCounterpart());
630 final AttachFileToConversationRunnable runnable = new AttachFileToConversationRunnable(this, uri, type, message, callback);
631 if (runnable.isVideoMessage()) {
632 VIDEO_COMPRESSION_EXECUTOR.execute(runnable);
633 } else {
634 FILE_ATTACHMENT_EXECUTOR.execute(runnable);
635 }
636 }
637
638 public void attachImageToConversation(final Conversation conversation, final Uri uri, final String type, final UiCallback<Message> callback) {
639 final String mimeType = MimeUtils.guessMimeTypeFromUriAndMime(this, uri, type);
640 final String compressPictures = getCompressPicturesPreference();
641
642 if ("never".equals(compressPictures)
643 || ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))
644 || (mimeType != null && mimeType.endsWith("/gif"))
645 || getFileBackend().unusualBounds(uri)) {
646 Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": not compressing picture. sending as file");
647 attachFileToConversation(conversation, uri, mimeType, callback);
648 return;
649 }
650 final Message message;
651 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
652 message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
653 } else {
654 message = new Message(conversation, "", conversation.getNextEncryption());
655 }
656 message.setThread(conversation.getThread());
657 if (!Message.configurePrivateFileMessage(message)) {
658 message.setCounterpart(conversation.getNextCounterpart());
659 message.setType(Message.TYPE_IMAGE);
660 }
661 Log.d(Config.LOGTAG, "attachImage: type=" + message.getType());
662 FILE_ATTACHMENT_EXECUTOR.execute(() -> {
663 try {
664 getFileBackend().copyImageToPrivateStorage(message, uri);
665 } catch (FileBackend.ImageCompressionException e) {
666 Log.d(Config.LOGTAG, "unable to compress image. fall back to file transfer", e);
667 attachFileToConversation(conversation, uri, mimeType, callback);
668 return;
669 } catch (final FileBackend.FileCopyException e) {
670 callback.error(e.getResId(), message);
671 return;
672 }
673 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
674 final PgpEngine pgpEngine = getPgpEngine();
675 if (pgpEngine != null) {
676 pgpEngine.encrypt(message, callback);
677 } else if (callback != null) {
678 callback.error(R.string.unable_to_connect_to_keychain, null);
679 }
680 } else {
681 sendMessage(message);
682 callback.success(message);
683 }
684 });
685 }
686
687 public Conversation find(Bookmark bookmark) {
688 return find(bookmark.getAccount(), bookmark.getJid());
689 }
690
691 public Conversation find(final Account account, final Jid jid) {
692 return find(getConversations(), account, jid);
693 }
694
695 public boolean isMuc(final Account account, final Jid jid) {
696 final Conversation c = find(account, jid);
697 return c != null && c.getMode() == Conversational.MODE_MULTI;
698 }
699
700 public void search(final List<String> term, final String uuid, final OnSearchResultsAvailable onSearchResultsAvailable) {
701 MessageSearchTask.search(this, term, uuid, onSearchResultsAvailable);
702 }
703
704 @Override
705 public int onStartCommand(Intent intent, int flags, int startId) {
706 final String action = intent == null ? null : intent.getAction();
707 final boolean needsForegroundService = intent != null && intent.getBooleanExtra(EventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE, false);
708 if (needsForegroundService) {
709 Log.d(Config.LOGTAG, "toggle forced foreground service after receiving event (action=" + action + ")");
710 toggleForegroundService(true);
711 }
712 String pushedAccountHash = null;
713 boolean interactive = false;
714 if (action != null) {
715 final String uuid = intent.getStringExtra("uuid");
716 switch (action) {
717 case QuickConversationsService.SMS_RETRIEVED_ACTION:
718 mQuickConversationsService.handleSmsReceived(intent);
719 break;
720 case ConnectivityManager.CONNECTIVITY_ACTION:
721 if (hasInternetConnection()) {
722 if (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0) {
723 schedulePostConnectivityChange();
724 }
725 if (Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
726 resetAllAttemptCounts(true, false);
727 }
728 Resolver.clearCache();
729 }
730 break;
731 case Intent.ACTION_SHUTDOWN:
732 logoutAndSave(true);
733 return START_NOT_STICKY;
734 case ACTION_CLEAR_MESSAGE_NOTIFICATION:
735 mNotificationExecutor.execute(() -> {
736 try {
737 final Conversation c = findConversationByUuid(uuid);
738 if (c != null) {
739 mNotificationService.clearMessages(c);
740 } else {
741 mNotificationService.clearMessages();
742 }
743 restoredFromDatabaseLatch.await();
744
745 } catch (InterruptedException e) {
746 Log.d(Config.LOGTAG, "unable to process clear message notification");
747 }
748 });
749 break;
750 case ACTION_CLEAR_MISSED_CALL_NOTIFICATION:
751 mNotificationExecutor.execute(() -> {
752 try {
753 final Conversation c = findConversationByUuid(uuid);
754 if (c != null) {
755 mNotificationService.clearMissedCalls(c);
756 } else {
757 mNotificationService.clearMissedCalls();
758 }
759 restoredFromDatabaseLatch.await();
760
761 } catch (InterruptedException e) {
762 Log.d(Config.LOGTAG, "unable to process clear missed call notification");
763 }
764 });
765 break;
766 case ACTION_DISMISS_CALL: {
767 final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
768 Log.d(Config.LOGTAG, "received intent to dismiss call with session id " + sessionId);
769 mJingleConnectionManager.rejectRtpSession(sessionId);
770 break;
771 }
772 case TorServiceUtils.ACTION_STATUS:
773 final String status = intent.getStringExtra(TorServiceUtils.EXTRA_STATUS);
774 //TODO port and host are in 'extras' - but this may not be a reliable source?
775 if ("ON".equals(status)) {
776 handleOrbotStartedEvent();
777 return START_STICKY;
778 }
779 break;
780 case ACTION_END_CALL: {
781 final String sessionId = intent.getStringExtra(RtpSessionActivity.EXTRA_SESSION_ID);
782 Log.d(Config.LOGTAG, "received intent to end call with session id " + sessionId);
783 mJingleConnectionManager.endRtpSession(sessionId);
784 }
785 break;
786 case ACTION_PROVISION_ACCOUNT: {
787 final String address = intent.getStringExtra("address");
788 final String password = intent.getStringExtra("password");
789 if (QuickConversationsService.isQuicksy() || Strings.isNullOrEmpty(address) || Strings.isNullOrEmpty(password)) {
790 break;
791 }
792 provisionAccount(address, password);
793 break;
794 }
795 case ACTION_DISMISS_ERROR_NOTIFICATIONS:
796 dismissErrorNotifications();
797 break;
798 case ACTION_TRY_AGAIN:
799 resetAllAttemptCounts(false, true);
800 interactive = true;
801 break;
802 case ACTION_REPLY_TO_CONVERSATION:
803 Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
804 if (remoteInput == null) {
805 break;
806 }
807 final CharSequence body = remoteInput.getCharSequence("text_reply");
808 final boolean dismissNotification = intent.getBooleanExtra("dismiss_notification", false);
809 final String lastMessageUuid = intent.getStringExtra("last_message_uuid");
810 if (body == null || body.length() <= 0) {
811 break;
812 }
813 mNotificationExecutor.execute(() -> {
814 try {
815 restoredFromDatabaseLatch.await();
816 final Conversation c = findConversationByUuid(uuid);
817 if (c != null) {
818 directReply(c, body.toString(), lastMessageUuid, dismissNotification);
819 }
820 } catch (InterruptedException e) {
821 Log.d(Config.LOGTAG, "unable to process direct reply");
822 }
823 });
824 break;
825 case ACTION_MARK_AS_READ:
826 mNotificationExecutor.execute(() -> {
827 final Conversation c = findConversationByUuid(uuid);
828 if (c == null) {
829 Log.d(Config.LOGTAG, "received mark read intent for unknown conversation (" + uuid + ")");
830 return;
831 }
832 try {
833 restoredFromDatabaseLatch.await();
834 sendReadMarker(c, null);
835 } catch (InterruptedException e) {
836 Log.d(Config.LOGTAG, "unable to process notification read marker for conversation " + c.getName());
837 }
838
839 });
840 break;
841 case ACTION_SNOOZE:
842 mNotificationExecutor.execute(() -> {
843 final Conversation c = findConversationByUuid(uuid);
844 if (c == null) {
845 Log.d(Config.LOGTAG, "received snooze intent for unknown conversation (" + uuid + ")");
846 return;
847 }
848 c.setMutedTill(System.currentTimeMillis() + 30 * 60 * 1000);
849 mNotificationService.clearMessages(c);
850 updateConversation(c);
851 });
852 case AudioManager.RINGER_MODE_CHANGED_ACTION:
853 case NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED:
854 if (dndOnSilentMode()) {
855 refreshAllPresences();
856 }
857 break;
858 case Intent.ACTION_SCREEN_ON:
859 deactivateGracePeriod();
860 case Intent.ACTION_USER_PRESENT:
861 case Intent.ACTION_SCREEN_OFF:
862 if (awayWhenScreenLocked()) {
863 refreshAllPresences();
864 }
865 break;
866 case ACTION_FCM_TOKEN_REFRESH:
867 refreshAllFcmTokens();
868 break;
869 case ACTION_RENEW_UNIFIED_PUSH_ENDPOINTS:
870 final String instance = intent.getStringExtra("instance");
871 final Optional<UnifiedPushBroker.Transport> transport = renewUnifiedPushEndpoints();
872 if (instance != null && transport.isPresent()) {
873 unifiedPushBroker.rebroadcastEndpoint(instance, transport.get());
874 }
875 break;
876 case ACTION_IDLE_PING:
877 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
878 scheduleNextIdlePing();
879 }
880 break;
881 case ACTION_FCM_MESSAGE_RECEIVED:
882 pushedAccountHash = intent.getStringExtra("account");
883 Log.d(Config.LOGTAG, "push message arrived in service. account=" + pushedAccountHash);
884 break;
885 case Intent.ACTION_SEND:
886 Uri uri = intent.getData();
887 if (uri != null) {
888 Log.d(Config.LOGTAG, "received uri permission for " + uri);
889 }
890 return START_STICKY;
891 }
892 }
893 synchronized (this) {
894 WakeLockHelper.acquire(wakeLock);
895 boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action) || (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL > 0 && ACTION_POST_CONNECTIVITY_CHANGE.equals(action));
896 final HashSet<Account> pingCandidates = new HashSet<>();
897 final String androidId = PhoneHelper.getAndroidId(this);
898 for (Account account : accounts) {
899 final boolean pushWasMeantForThisAccount = CryptoHelper.getAccountFingerprint(account, androidId).equals(pushedAccountHash);
900 pingNow |= processAccountState(account,
901 interactive,
902 "ui".equals(action),
903 pushWasMeantForThisAccount,
904 pingCandidates);
905 }
906 if (pingNow) {
907 for (Account account : pingCandidates) {
908 final boolean lowTimeout = isInLowPingTimeoutMode(account);
909 account.getXmppConnection().sendPing();
910 Log.d(Config.LOGTAG, account.getJid().asBareJid() + " send ping (action=" + action + ",lowTimeout=" + lowTimeout + ")");
911 scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode());
912 }
913 }
914 WakeLockHelper.release(wakeLock);
915 }
916 if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) {
917 expireOldMessages();
918 }
919 return START_STICKY;
920 }
921
922 private void handleOrbotStartedEvent() {
923 for (final Account account : accounts) {
924 if (account.getStatus() == Account.State.TOR_NOT_AVAILABLE) {
925 reconnectAccount(account, true, false);
926 }
927 }
928 }
929
930 private boolean processAccountState(Account account, boolean interactive, boolean isUiAction, boolean isAccountPushed, HashSet<Account> pingCandidates) {
931 boolean pingNow = false;
932 if (account.getStatus().isAttemptReconnect()) {
933 if (!hasInternetConnection()) {
934 account.setStatus(Account.State.NO_INTERNET);
935 if (statusListener != null) {
936 statusListener.onStatusChanged(account);
937 }
938 } else {
939 if (account.getStatus() == Account.State.NO_INTERNET) {
940 account.setStatus(Account.State.OFFLINE);
941 if (statusListener != null) {
942 statusListener.onStatusChanged(account);
943 }
944 }
945 if (account.getStatus() == Account.State.ONLINE) {
946 synchronized (mLowPingTimeoutMode) {
947 long lastReceived = account.getXmppConnection().getLastPacketReceived();
948 long lastSent = account.getXmppConnection().getLastPingSent();
949 long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
950 long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime();
951 int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().asBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000;
952 long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
953 if (lastSent > lastReceived) {
954 if (pingTimeoutIn < 0) {
955 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ping timeout");
956 this.reconnectAccount(account, true, interactive);
957 } else {
958 int secs = (int) (pingTimeoutIn / 1000);
959 this.scheduleWakeUpCall(secs, account.getUuid().hashCode());
960 }
961 } else {
962 pingCandidates.add(account);
963 if (isAccountPushed) {
964 pingNow = true;
965 if (mLowPingTimeoutMode.add(account.getJid().asBareJid())) {
966 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": entering low ping timeout mode");
967 }
968 } else if (msToNextPing <= 0) {
969 pingNow = true;
970 } else {
971 this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
972 if (mLowPingTimeoutMode.remove(account.getJid().asBareJid())) {
973 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": leaving low ping timeout mode");
974 }
975 }
976 }
977 }
978 } else if (account.getStatus() == Account.State.OFFLINE) {
979 reconnectAccount(account, true, interactive);
980 } else if (account.getStatus() == Account.State.CONNECTING) {
981 long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
982 long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
983 long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
984 long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
985 if (timeout < 0) {
986 Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast=" + secondsSinceLastConnect + ")");
987 account.getXmppConnection().resetAttemptCount(false);
988 reconnectAccount(account, true, interactive);
989 } else if (discoTimeout < 0) {
990 account.getXmppConnection().sendDiscoTimeout();
991 scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode());
992 } else {
993 scheduleWakeUpCall((int) Math.min(timeout, discoTimeout), account.getUuid().hashCode());
994 }
995 } else {
996 if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
997 reconnectAccount(account, true, interactive);
998 }
999 }
1000 }
1001 }
1002 return pingNow;
1003 }
1004
1005 public boolean processUnifiedPushMessage(final Account account, final Jid transport, final Element push) {
1006 return unifiedPushBroker.processPushMessage(account, transport, push);
1007 }
1008
1009 public void reinitializeMuclumbusService() {
1010 mChannelDiscoveryService.initializeMuclumbusService();
1011 }
1012
1013 public void discoverChannels(String query, ChannelDiscoveryService.Method method, ChannelDiscoveryService.OnChannelSearchResultsFound onChannelSearchResultsFound) {
1014 mChannelDiscoveryService.discover(Strings.nullToEmpty(query).trim(), method, onChannelSearchResultsFound);
1015 }
1016
1017 public boolean isDataSaverDisabled() {
1018 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1019 final ConnectivityManager connectivityManager =
1020 (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
1021 return !connectivityManager.isActiveNetworkMetered()
1022 || Compatibility.getRestrictBackgroundStatus(connectivityManager)
1023 == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
1024 } else {
1025 return true;
1026 }
1027 }
1028
1029 private void directReply(final Conversation conversation, final String body, final String lastMessageUuid, final boolean dismissAfterReply) {
1030 final Message inReplyTo = lastMessageUuid == null ? null : conversation.findMessageWithUuid(lastMessageUuid);
1031 final Message message = new Message(conversation, body, conversation.getNextEncryption());
1032 if (inReplyTo != null) message.setThread(inReplyTo.getThread());
1033 if (inReplyTo != null && inReplyTo.isPrivateMessage()) {
1034 Message.configurePrivateMessage(message, inReplyTo.getCounterpart());
1035 }
1036 message.markUnread();
1037 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
1038 getPgpEngine().encrypt(message, new UiCallback<Message>() {
1039 @Override
1040 public void success(Message message) {
1041 if (dismissAfterReply) {
1042 markRead((Conversation) message.getConversation(), true);
1043 } else {
1044 mNotificationService.pushFromDirectReply(message);
1045 }
1046 }
1047
1048 @Override
1049 public void error(int errorCode, Message object) {
1050
1051 }
1052
1053 @Override
1054 public void userInputRequired(PendingIntent pi, Message object) {
1055
1056 }
1057 });
1058 } else {
1059 sendMessage(message);
1060 if (dismissAfterReply) {
1061 markRead(conversation, true);
1062 } else {
1063 mNotificationService.pushFromDirectReply(message);
1064 }
1065 }
1066 }
1067
1068 private boolean dndOnSilentMode() {
1069 return getBooleanPreference(SettingsActivity.DND_ON_SILENT_MODE, R.bool.dnd_on_silent_mode);
1070 }
1071
1072 private boolean manuallyChangePresence() {
1073 return getBooleanPreference(SettingsActivity.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
1074 }
1075
1076 private boolean treatVibrateAsSilent() {
1077 return getBooleanPreference(SettingsActivity.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent);
1078 }
1079
1080 private boolean awayWhenScreenLocked() {
1081 return getBooleanPreference(SettingsActivity.AWAY_WHEN_SCREEN_IS_OFF, R.bool.away_when_screen_off);
1082 }
1083
1084 private String getCompressPicturesPreference() {
1085 return getPreferences().getString("picture_compression", getResources().getString(R.string.picture_compression));
1086 }
1087
1088 private Presence.Status getTargetPresence() {
1089 if (dndOnSilentMode() && isPhoneSilenced()) {
1090 return Presence.Status.DND;
1091 } else if (awayWhenScreenLocked() && isScreenLocked()) {
1092 return Presence.Status.AWAY;
1093 } else {
1094 return Presence.Status.ONLINE;
1095 }
1096 }
1097
1098 public boolean isScreenLocked() {
1099 final KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
1100 final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
1101 final boolean locked = keyguardManager != null && keyguardManager.isKeyguardLocked();
1102 final boolean interactive = powerManager != null && powerManager.isInteractive();
1103 return locked || !interactive;
1104 }
1105
1106 private boolean isPhoneSilenced() {
1107 final boolean notificationDnd;
1108 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1109 final NotificationManager notificationManager = getSystemService(NotificationManager.class);
1110 final int filter = notificationManager == null ? NotificationManager.INTERRUPTION_FILTER_UNKNOWN : notificationManager.getCurrentInterruptionFilter();
1111 notificationDnd = filter >= NotificationManager.INTERRUPTION_FILTER_PRIORITY;
1112 } else {
1113 notificationDnd = false;
1114 }
1115 final AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
1116 final int ringerMode = audioManager == null ? AudioManager.RINGER_MODE_NORMAL : audioManager.getRingerMode();
1117 try {
1118 if (treatVibrateAsSilent()) {
1119 return notificationDnd || ringerMode != AudioManager.RINGER_MODE_NORMAL;
1120 } else {
1121 return notificationDnd || ringerMode == AudioManager.RINGER_MODE_SILENT;
1122 }
1123 } catch (Throwable throwable) {
1124 Log.d(Config.LOGTAG, "platform bug in isPhoneSilenced (" + throwable.getMessage() + ")");
1125 return notificationDnd;
1126 }
1127 }
1128
1129 private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) {
1130 Log.d(Config.LOGTAG, "resetting all attempt counts");
1131 for (Account account : accounts) {
1132 if (account.hasErrorStatus() || reallyAll) {
1133 final XmppConnection connection = account.getXmppConnection();
1134 if (connection != null) {
1135 connection.resetAttemptCount(retryImmediately);
1136 }
1137 }
1138 if (account.setShowErrorNotification(true)) {
1139 mDatabaseWriterExecutor.execute(() -> databaseBackend.updateAccount(account));
1140 }
1141 }
1142 mNotificationService.updateErrorNotification();
1143 }
1144
1145 private void dismissErrorNotifications() {
1146 for (final Account account : this.accounts) {
1147 if (account.hasErrorStatus()) {
1148 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": dismissing error notification");
1149 if (account.setShowErrorNotification(false)) {
1150 mDatabaseWriterExecutor.execute(() -> databaseBackend.updateAccount(account));
1151 }
1152 }
1153 }
1154 }
1155
1156 private void expireOldMessages() {
1157 expireOldMessages(false);
1158 }
1159
1160 public void expireOldMessages(final boolean resetHasMessagesLeftOnServer) {
1161 mLastExpiryRun.set(SystemClock.elapsedRealtime());
1162 mDatabaseWriterExecutor.execute(() -> {
1163 long timestamp = getAutomaticMessageDeletionDate();
1164 if (timestamp > 0) {
1165 databaseBackend.expireOldMessages(timestamp);
1166 synchronized (XmppConnectionService.this.conversations) {
1167 for (Conversation conversation : XmppConnectionService.this.conversations) {
1168 conversation.expireOldMessages(timestamp);
1169 if (resetHasMessagesLeftOnServer) {
1170 conversation.messagesLoaded.set(true);
1171 conversation.setHasMessagesLeftOnServer(true);
1172 }
1173 }
1174 }
1175 updateConversationUi();
1176 }
1177 });
1178 }
1179
1180 public boolean hasInternetConnection() {
1181 final ConnectivityManager cm = ContextCompat.getSystemService(this, ConnectivityManager.class);
1182 if (cm == null) {
1183 return true; //if internet connection can not be checked it is probably best to just try
1184 }
1185 try {
1186 if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
1187 final Network activeNetwork = cm.getActiveNetwork();
1188 final NetworkCapabilities capabilities = activeNetwork == null ? null : cm.getNetworkCapabilities(activeNetwork);
1189 return capabilities != null && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
1190 } else {
1191 final NetworkInfo networkInfo = cm.getActiveNetworkInfo();
1192 return networkInfo != null && (networkInfo.isConnected() || networkInfo.getType() == ConnectivityManager.TYPE_ETHERNET);
1193 }
1194 } catch (final RuntimeException e) {
1195 Log.d(Config.LOGTAG, "unable to check for internet connection", e);
1196 return true; //if internet connection can not be checked it is probably best to just try
1197 }
1198 }
1199
1200 @SuppressLint("TrulyRandom")
1201 @Override
1202 public void onCreate() {
1203 setTheme(ThemeHelper.find(this));
1204 ThemeHelper.applyCustomColors(this);
1205 if (Compatibility.runsTwentySix()) {
1206 mNotificationService.initializeChannels();
1207 }
1208 mChannelDiscoveryService.initializeMuclumbusService();
1209 mForceDuringOnCreate.set(Compatibility.runsAndTargetsTwentySix(this));
1210 toggleForegroundService();
1211 this.destroyed = false;
1212 OmemoSetting.load(this);
1213 ExceptionHelper.init(getApplicationContext());
1214 try {
1215 Security.insertProviderAt(Conscrypt.newProvider(), 1);
1216 } catch (Throwable throwable) {
1217 Log.e(Config.LOGTAG, "unable to initialize security provider", throwable);
1218 }
1219 Resolver.init(this);
1220 updateMemorizingTrustmanager();
1221 final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
1222 final int cacheSize = maxMemory / 9;
1223 this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
1224 @Override
1225 protected int sizeOf(final String key, final Bitmap bitmap) {
1226 return bitmap.getByteCount() / 1024;
1227 }
1228 };
1229 this.mDrawableCache = new LruCache<String, Drawable>(cacheSize) {
1230 @Override
1231 protected int sizeOf(final String key, final Drawable drawable) {
1232 if (drawable instanceof BitmapDrawable) {
1233 return ((BitmapDrawable) drawable).getBitmap().getByteCount() / 1024;
1234 } else {
1235 return drawable.getIntrinsicWidth() * drawable.getIntrinsicHeight() * 40 / 1024;
1236 }
1237 }
1238 };
1239 if (mLastActivity == 0) {
1240 mLastActivity = getPreferences().getLong(SETTING_LAST_ACTIVITY_TS, System.currentTimeMillis());
1241 }
1242
1243 Log.d(Config.LOGTAG, "initializing database...");
1244 this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
1245 Log.d(Config.LOGTAG, "restoring accounts...");
1246 this.accounts = databaseBackend.getAccounts();
1247 final SharedPreferences.Editor editor = getPreferences().edit();
1248 if (this.accounts.size() == 0 && Arrays.asList("Sony", "Sony Ericsson").contains(Build.MANUFACTURER)) {
1249 editor.putBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE, true);
1250 Log.d(Config.LOGTAG, Build.MANUFACTURER + " is on blacklist. enabling foreground service");
1251 }
1252 final boolean hasEnabledAccounts = hasEnabledAccounts();
1253 editor.putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
1254 editor.apply();
1255 toggleSetProfilePictureActivity(hasEnabledAccounts);
1256 reconfigurePushDistributor();
1257
1258 restoreFromDatabase();
1259
1260 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
1261 startContactObserver();
1262 }
1263 FILE_OBSERVER_EXECUTOR.execute(fileBackend::deleteHistoricAvatarPath);
1264 if (Compatibility.hasStoragePermission(this)) {
1265 Log.d(Config.LOGTAG, "starting file observer");
1266 FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::startWatching);
1267 FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
1268 }
1269 if (Config.supportOpenPgp()) {
1270 this.pgpServiceConnection = new OpenPgpServiceConnection(this, "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() {
1271 @Override
1272 public void onBound(IOpenPgpService2 service) {
1273 for (Account account : accounts) {
1274 final PgpDecryptionService pgp = account.getPgpDecryptionService();
1275 if (pgp != null) {
1276 pgp.continueDecryption(true);
1277 }
1278 }
1279 }
1280
1281 @Override
1282 public void onError(Exception e) {
1283 }
1284 });
1285 this.pgpServiceConnection.bindToService();
1286 }
1287
1288 final PowerManager pm = ContextCompat.getSystemService(this, PowerManager.class);
1289 this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Conversations:Service");
1290
1291 toggleForegroundService();
1292 updateUnreadCountBadge();
1293 toggleScreenEventReceiver();
1294 final IntentFilter intentFilter = new IntentFilter();
1295 intentFilter.addAction(TorServiceUtils.ACTION_STATUS);
1296 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1297 scheduleNextIdlePing();
1298 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
1299 intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
1300 }
1301 intentFilter.addAction(NotificationManager.ACTION_INTERRUPTION_FILTER_CHANGED);
1302 }
1303 registerReceiver(this.mInternalEventReceiver, intentFilter);
1304 mForceDuringOnCreate.set(false);
1305 toggleForegroundService();
1306 setupPhoneStateListener();
1307 }
1308
1309
1310 private void setupPhoneStateListener() {
1311 final TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
1312 if (telephonyManager == null || Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
1313 return;
1314 }
1315 telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
1316 }
1317
1318 public boolean isPhoneInCall() {
1319 return isPhoneInCall.get();
1320 }
1321
1322 private void checkForDeletedFiles() {
1323 if (destroyed) {
1324 Log.d(Config.LOGTAG, "Do not check for deleted files because service has been destroyed");
1325 return;
1326 }
1327 final long start = SystemClock.elapsedRealtime();
1328 final List<DatabaseBackend.FilePathInfo> relativeFilePaths = databaseBackend.getFilePathInfo();
1329 final List<DatabaseBackend.FilePathInfo> changed = new ArrayList<>();
1330 for (final DatabaseBackend.FilePathInfo filePath : relativeFilePaths) {
1331 if (destroyed) {
1332 Log.d(Config.LOGTAG, "Stop checking for deleted files because service has been destroyed");
1333 return;
1334 }
1335 final File file = fileBackend.getFileForPath(filePath.path);
1336 if (filePath.setDeleted(!file.exists())) {
1337 changed.add(filePath);
1338 }
1339 }
1340 final long duration = SystemClock.elapsedRealtime() - start;
1341 Log.d(Config.LOGTAG, "found " + changed.size() + " changed files on start up. total=" + relativeFilePaths.size() + ". (" + duration + "ms)");
1342 if (changed.size() > 0) {
1343 databaseBackend.markFilesAsChanged(changed);
1344 markChangedFiles(changed);
1345 }
1346 }
1347
1348 public void startContactObserver() {
1349 getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, new ContentObserver(null) {
1350 @Override
1351 public void onChange(boolean selfChange) {
1352 super.onChange(selfChange);
1353 if (restoredFromDatabaseLatch.getCount() == 0) {
1354 loadPhoneContacts();
1355 }
1356 }
1357 });
1358 }
1359
1360 @Override
1361 public void onTrimMemory(int level) {
1362 super.onTrimMemory(level);
1363 if (level >= TRIM_MEMORY_COMPLETE) {
1364 Log.d(Config.LOGTAG, "clear cache due to low memory");
1365 getBitmapCache().evictAll();
1366 }
1367 }
1368
1369 @Override
1370 public void onDestroy() {
1371 try {
1372 unregisterReceiver(this.mInternalEventReceiver);
1373 unregisterReceiver(this.mInternalScreenEventReceiver);
1374 } catch (final IllegalArgumentException e) {
1375 //ignored
1376 }
1377 destroyed = false;
1378 fileObserver.stopWatching();
1379 super.onDestroy();
1380 }
1381
1382 public void restartFileObserver() {
1383 Log.d(Config.LOGTAG, "restarting file observer");
1384 FILE_OBSERVER_EXECUTOR.execute(this.fileObserver::restartWatching);
1385 FILE_OBSERVER_EXECUTOR.execute(this::checkForDeletedFiles);
1386 }
1387
1388 public void toggleScreenEventReceiver() {
1389 if (awayWhenScreenLocked() && !manuallyChangePresence()) {
1390 final IntentFilter filter = new IntentFilter();
1391 filter.addAction(Intent.ACTION_SCREEN_ON);
1392 filter.addAction(Intent.ACTION_SCREEN_OFF);
1393 filter.addAction(Intent.ACTION_USER_PRESENT);
1394 registerReceiver(this.mInternalScreenEventReceiver, filter);
1395 } else {
1396 try {
1397 unregisterReceiver(this.mInternalScreenEventReceiver);
1398 } catch (IllegalArgumentException e) {
1399 //ignored
1400 }
1401 }
1402 }
1403
1404 public void toggleForegroundService() {
1405 toggleForegroundService(false);
1406 }
1407
1408 public void setOngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
1409 ongoingCall.set(new OngoingCall(id, media, reconnecting));
1410 toggleForegroundService(false);
1411 }
1412
1413 public void removeOngoingCall() {
1414 ongoingCall.set(null);
1415 toggleForegroundService(false);
1416 }
1417
1418 private void toggleForegroundService(boolean force) {
1419 final boolean status;
1420 final OngoingCall ongoing = ongoingCall.get();
1421 final boolean showOngoing = ongoing != null && !diallerIntegrationActive.get();
1422 if (force || mForceDuringOnCreate.get() || mForceForegroundService.get() || showOngoing || (Compatibility.keepForegroundService(this) && hasEnabledAccounts())) {
1423 final Notification notification;
1424 final int id;
1425 if (showOngoing) {
1426 notification = this.mNotificationService.getOngoingCallNotification(ongoing);
1427 id = NotificationService.ONGOING_CALL_NOTIFICATION_ID;
1428 startForeground(id, notification);
1429 mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID);
1430 } else {
1431 notification = this.mNotificationService.createForegroundNotification();
1432 id = NotificationService.FOREGROUND_NOTIFICATION_ID;
1433 startForeground(id, notification);
1434 }
1435
1436 if (!mForceForegroundService.get()) {
1437 mNotificationService.notify(id, notification);
1438 }
1439 status = true;
1440 } else {
1441 stopForeground(true);
1442 status = false;
1443 }
1444 if (!mForceForegroundService.get()) {
1445 mNotificationService.cancel(NotificationService.FOREGROUND_NOTIFICATION_ID);
1446 }
1447 if (!showOngoing) {
1448 mNotificationService.cancel(NotificationService.ONGOING_CALL_NOTIFICATION_ID);
1449 }
1450 Log.d(Config.LOGTAG, "ForegroundService: " + (status ? "on" : "off"));
1451 }
1452
1453 public boolean foregroundNotificationNeedsUpdatingWhenErrorStateChanges() {
1454 return !mForceForegroundService.get() && ongoingCall.get() == null && Compatibility.keepForegroundService(this) && hasEnabledAccounts();
1455 }
1456
1457 @Override
1458 public void onTaskRemoved(final Intent rootIntent) {
1459 super.onTaskRemoved(rootIntent);
1460 if ((Compatibility.keepForegroundService(this) && hasEnabledAccounts()) || mForceForegroundService.get() || ongoingCall.get() != null) {
1461 Log.d(Config.LOGTAG, "ignoring onTaskRemoved because foreground service is activated");
1462 } else {
1463 this.logoutAndSave(false);
1464 }
1465 }
1466
1467 private void logoutAndSave(boolean stop) {
1468 int activeAccounts = 0;
1469 for (final Account account : accounts) {
1470 if (account.getStatus() != Account.State.DISABLED) {
1471 databaseBackend.writeRoster(account.getRoster());
1472 activeAccounts++;
1473 }
1474 if (account.getXmppConnection() != null) {
1475 new Thread(() -> disconnect(account, false)).start();
1476 }
1477 }
1478 if (stop || activeAccounts == 0) {
1479 Log.d(Config.LOGTAG, "good bye");
1480 stopSelf();
1481 }
1482 }
1483
1484 private void schedulePostConnectivityChange() {
1485 final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
1486 if (alarmManager == null) {
1487 return;
1488 }
1489 final long triggerAtMillis = SystemClock.elapsedRealtime() + (Config.POST_CONNECTIVITY_CHANGE_PING_INTERVAL * 1000);
1490 final Intent intent = new Intent(this, EventReceiver.class);
1491 intent.setAction(ACTION_POST_CONNECTIVITY_CHANGE);
1492 try {
1493 final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 1, intent, s()
1494 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1495 : PendingIntent.FLAG_UPDATE_CURRENT);
1496 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1497 alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
1498 } else {
1499 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, triggerAtMillis, pendingIntent);
1500 }
1501 } catch (RuntimeException e) {
1502 Log.e(Config.LOGTAG, "unable to schedule alarm for post connectivity change", e);
1503 }
1504 }
1505
1506 public void scheduleWakeUpCall(int seconds, int requestCode) {
1507 final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000L;
1508 final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
1509 if (alarmManager == null) {
1510 return;
1511 }
1512 final Intent intent = new Intent(this, EventReceiver.class);
1513 intent.setAction("ping");
1514 try {
1515 final PendingIntent pendingIntent;
1516 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
1517 pendingIntent =
1518 PendingIntent.getBroadcast(
1519 this, requestCode, intent, PendingIntent.FLAG_IMMUTABLE);
1520 } else {
1521 pendingIntent =
1522 PendingIntent.getBroadcast(
1523 this, requestCode, intent, 0);
1524 }
1525 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
1526 } catch (RuntimeException e) {
1527 Log.e(Config.LOGTAG, "unable to schedule alarm for ping", e);
1528 }
1529 }
1530
1531 @TargetApi(Build.VERSION_CODES.M)
1532 private void scheduleNextIdlePing() {
1533 final long timeToWake = SystemClock.elapsedRealtime() + (Config.IDLE_PING_INTERVAL * 1000);
1534 final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
1535 if (alarmManager == null) {
1536 return;
1537 }
1538 final Intent intent = new Intent(this, EventReceiver.class);
1539 intent.setAction(ACTION_IDLE_PING);
1540 try {
1541 final PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, s()
1542 ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
1543 : PendingIntent.FLAG_UPDATE_CURRENT);
1544 alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, pendingIntent);
1545 } catch (RuntimeException e) {
1546 Log.d(Config.LOGTAG, "unable to schedule alarm for idle ping", e);
1547 }
1548 }
1549
1550 public XmppConnection createConnection(final Account account) {
1551 final XmppConnection connection = new XmppConnection(account, this);
1552 connection.setOnMessagePacketReceivedListener(this.mMessageParser);
1553 connection.setOnStatusChangedListener(this.statusListener);
1554 connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
1555 connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
1556 connection.setOnJinglePacketReceivedListener((mJingleConnectionManager::deliverPacket));
1557 connection.setOnBindListener(this.mOnBindListener);
1558 connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
1559 connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
1560 connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService);
1561 AxolotlService axolotlService = account.getAxolotlService();
1562 if (axolotlService != null) {
1563 connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
1564 }
1565 return connection;
1566 }
1567
1568 public void sendChatState(Conversation conversation) {
1569 if (sendChatStates()) {
1570 MessagePacket packet = mMessageGenerator.generateChatState(conversation);
1571 sendMessagePacket(conversation.getAccount(), packet);
1572 }
1573 }
1574
1575 private void sendFileMessage(final Message message, final boolean delay) {
1576 Log.d(Config.LOGTAG, "send file message");
1577 final Account account = message.getConversation().getAccount();
1578 if (account.httpUploadAvailable(fileBackend.getFile(message, false).getSize())
1579 || message.getConversation().getMode() == Conversation.MODE_MULTI) {
1580 mHttpConnectionManager.createNewUploadConnection(message, delay);
1581 } else {
1582 mJingleConnectionManager.startJingleFileTransfer(message);
1583 }
1584 }
1585
1586 public void sendMessage(final Message message) {
1587 sendMessage(message, false, false);
1588 }
1589
1590 private void sendMessage(final Message message, final boolean resend, final boolean delay) {
1591 final Account account = message.getConversation().getAccount();
1592 if (account.setShowErrorNotification(true)) {
1593 databaseBackend.updateAccount(account);
1594 mNotificationService.updateErrorNotification();
1595 }
1596 final Conversation conversation = (Conversation) message.getConversation();
1597 account.deactivateGracePeriod();
1598
1599
1600 if (QuickConversationsService.isQuicksy() && conversation.getMode() == Conversation.MODE_SINGLE) {
1601 final Contact contact = conversation.getContact();
1602 if (!contact.showInRoster() && contact.getOption(Contact.Options.SYNCED_VIA_OTHER)) {
1603 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": adding " + contact.getJid() + " on sending message");
1604 createContact(contact, true);
1605 }
1606 }
1607
1608 MessagePacket packet = null;
1609 final boolean addToConversation = !message.edited();
1610 boolean saveInDb = addToConversation;
1611 message.setStatus(Message.STATUS_WAITING);
1612
1613 if (message.getEncryption() != Message.ENCRYPTION_NONE && conversation.getMode() == Conversation.MODE_MULTI && conversation.isPrivateAndNonAnonymous()) {
1614 if (conversation.setAttribute(Conversation.ATTRIBUTE_FORMERLY_PRIVATE_NON_ANONYMOUS, true)) {
1615 databaseBackend.updateConversation(conversation);
1616 }
1617 }
1618
1619 final boolean inProgressJoin = isJoinInProgress(conversation);
1620
1621 if (message.getCounterpart() == null && !message.isPrivateMessage()) {
1622 message.setCounterpart(message.getConversation().getJid().asBareJid());
1623 }
1624
1625 if (account.isOnlineAndConnected() && !inProgressJoin) {
1626 switch (message.getEncryption()) {
1627 case Message.ENCRYPTION_NONE:
1628 if (message.needsUploading()) {
1629 if (account.httpUploadAvailable(fileBackend.getFile(message, false).getSize())
1630 || conversation.getMode() == Conversation.MODE_MULTI
1631 || message.fixCounterpart()) {
1632 this.sendFileMessage(message, delay);
1633 } else {
1634 break;
1635 }
1636 } else {
1637 packet = mMessageGenerator.generateChat(message);
1638 }
1639 break;
1640 case Message.ENCRYPTION_PGP:
1641 case Message.ENCRYPTION_DECRYPTED:
1642 if (message.needsUploading()) {
1643 if (account.httpUploadAvailable(fileBackend.getFile(message, false).getSize())
1644 || conversation.getMode() == Conversation.MODE_MULTI
1645 || message.fixCounterpart()) {
1646 this.sendFileMessage(message, delay);
1647 } else {
1648 break;
1649 }
1650 } else {
1651 packet = mMessageGenerator.generatePgpChat(message);
1652 }
1653 break;
1654 case Message.ENCRYPTION_AXOLOTL:
1655 message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
1656 if (message.needsUploading()) {
1657 if (account.httpUploadAvailable(fileBackend.getFile(message, false).getSize())
1658 || conversation.getMode() == Conversation.MODE_MULTI
1659 || message.fixCounterpart()) {
1660 this.sendFileMessage(message, delay);
1661 } else {
1662 break;
1663 }
1664 } else {
1665 XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message);
1666 if (axolotlMessage == null) {
1667 account.getAxolotlService().preparePayloadMessage(message, delay);
1668 } else {
1669 packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
1670 }
1671 }
1672 break;
1673
1674 }
1675 if (packet != null) {
1676 if (account.getXmppConnection().getFeatures().sm()
1677 || (conversation.getMode() == Conversation.MODE_MULTI && message.getCounterpart().isBareJid())) {
1678 message.setStatus(Message.STATUS_UNSEND);
1679 } else {
1680 message.setStatus(Message.STATUS_SEND);
1681 }
1682 }
1683 } else {
1684 switch (message.getEncryption()) {
1685 case Message.ENCRYPTION_DECRYPTED:
1686 if (!message.needsUploading()) {
1687 String pgpBody = message.getEncryptedBody();
1688 String decryptedBody = message.getBody();
1689 message.setBody(pgpBody); //TODO might throw NPE
1690 message.setEncryption(Message.ENCRYPTION_PGP);
1691 if (message.edited()) {
1692 message.setBody(decryptedBody);
1693 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
1694 if (!databaseBackend.updateMessage(message, message.getEditedId())) {
1695 Log.e(Config.LOGTAG, "error updated message in DB after edit");
1696 }
1697 updateConversationUi();
1698 return;
1699 } else {
1700 databaseBackend.createMessage(message);
1701 saveInDb = false;
1702 message.setBody(decryptedBody);
1703 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
1704 }
1705 }
1706 break;
1707 case Message.ENCRYPTION_AXOLOTL:
1708 message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
1709 break;
1710 }
1711 }
1712
1713
1714 boolean mucMessage = conversation.getMode() == Conversation.MODE_MULTI && !message.isPrivateMessage();
1715 if (mucMessage) {
1716 message.setCounterpart(conversation.getMucOptions().getSelf().getFullJid());
1717 }
1718
1719 if (resend) {
1720 if (packet != null && addToConversation) {
1721 if (account.getXmppConnection().getFeatures().sm() || mucMessage) {
1722 markMessage(message, Message.STATUS_UNSEND);
1723 } else {
1724 markMessage(message, Message.STATUS_SEND);
1725 }
1726 }
1727 } else {
1728 if (addToConversation) {
1729 conversation.add(message);
1730 }
1731 if (saveInDb) {
1732 databaseBackend.createMessage(message);
1733 } else if (message.edited()) {
1734 if (!databaseBackend.updateMessage(message, message.getEditedId())) {
1735 Log.e(Config.LOGTAG, "error updated message in DB after edit");
1736 }
1737 }
1738 updateConversationUi();
1739 }
1740 if (packet != null) {
1741 if (delay) {
1742 mMessageGenerator.addDelay(packet, message.getTimeSent());
1743 }
1744 if (conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
1745 if (this.sendChatStates()) {
1746 packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
1747 }
1748 }
1749 sendMessagePacket(account, packet);
1750 }
1751 }
1752
1753 private boolean isJoinInProgress(final Conversation conversation) {
1754 final Account account = conversation.getAccount();
1755 synchronized (account.inProgressConferenceJoins) {
1756 if (conversation.getMode() == Conversational.MODE_MULTI) {
1757 final boolean inProgress = account.inProgressConferenceJoins.contains(conversation);
1758 final boolean pending = account.pendingConferenceJoins.contains(conversation);
1759 final boolean inProgressJoin = inProgress || pending;
1760 if (inProgressJoin) {
1761 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": holding back message to group. inProgress=" + inProgress + ", pending=" + pending);
1762 }
1763 return inProgressJoin;
1764 } else {
1765 return false;
1766 }
1767 }
1768 }
1769
1770 private void sendUnsentMessages(final Conversation conversation) {
1771 conversation.findWaitingMessages(message -> resendMessage(message, true));
1772 }
1773
1774 public void resendMessage(final Message message, final boolean delay) {
1775 sendMessage(message, true, delay);
1776 }
1777
1778 public void requestEasyOnboardingInvite(final Account account, final EasyOnboardingInvite.OnInviteRequested callback) {
1779 final XmppConnection connection = account.getXmppConnection();
1780 final Jid jid = connection == null ? null : connection.getJidForCommand(Namespace.EASY_ONBOARDING_INVITE);
1781 if (jid == null) {
1782 callback.inviteRequestFailed(getString(R.string.server_does_not_support_easy_onboarding_invites));
1783 return;
1784 }
1785 final IqPacket request = new IqPacket(IqPacket.TYPE.SET);
1786 request.setTo(jid);
1787 final Element command = request.addChild("command", Namespace.COMMANDS);
1788 command.setAttribute("node", Namespace.EASY_ONBOARDING_INVITE);
1789 command.setAttribute("action", "execute");
1790 sendIqPacket(account, request, (a, response) -> {
1791 if (response.getType() == IqPacket.TYPE.RESULT) {
1792 final Element resultCommand = response.findChild("command", Namespace.COMMANDS);
1793 final Element x = resultCommand == null ? null : resultCommand.findChild("x", Namespace.DATA);
1794 if (x != null) {
1795 final Data data = Data.parse(x);
1796 final String uri = data.getValue("uri");
1797 final String landingUrl = data.getValue("landing-url");
1798 if (uri != null) {
1799 final EasyOnboardingInvite invite = new EasyOnboardingInvite(jid.getDomain().toEscapedString(), uri, landingUrl);
1800 callback.inviteRequested(invite);
1801 return;
1802 }
1803 }
1804 callback.inviteRequestFailed(getString(R.string.unable_to_parse_invite));
1805 Log.d(Config.LOGTAG, response.toString());
1806 } else if (response.getType() == IqPacket.TYPE.ERROR) {
1807 callback.inviteRequestFailed(IqParser.errorMessage(response));
1808 } else {
1809 callback.inviteRequestFailed(getString(R.string.remote_server_timeout));
1810 }
1811 });
1812
1813 }
1814
1815 public void fetchRosterFromServer(final Account account) {
1816 final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
1817 if (!"".equals(account.getRosterVersion())) {
1818 Log.d(Config.LOGTAG, account.getJid().asBareJid()
1819 + ": fetching roster version " + account.getRosterVersion());
1820 } else {
1821 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching roster");
1822 }
1823 iqPacket.query(Namespace.ROSTER).setAttribute("ver", account.getRosterVersion());
1824 sendIqPacket(account, iqPacket, mIqParser);
1825 }
1826
1827 public void fetchBookmarks(final Account account) {
1828 final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
1829 final Element query = iqPacket.query("jabber:iq:private");
1830 query.addChild("storage", Namespace.BOOKMARKS);
1831 final OnIqPacketReceived callback = (a, response) -> {
1832 if (response.getType() == IqPacket.TYPE.RESULT) {
1833 final Element query1 = response.query();
1834 final Element storage = query1.findChild("storage", "storage:bookmarks");
1835 Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
1836 processBookmarksInitial(a, bookmarks, false);
1837 } else {
1838 Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": could not fetch bookmarks");
1839 }
1840 };
1841 sendIqPacket(account, iqPacket, callback);
1842 }
1843
1844 public void fetchBookmarks2(final Account account) {
1845 final IqPacket retrieve = mIqGenerator.retrieveBookmarks();
1846 sendIqPacket(account, retrieve, new OnIqPacketReceived() {
1847 @Override
1848 public void onIqPacketReceived(final Account account, final IqPacket response) {
1849 if (response.getType() == IqPacket.TYPE.RESULT) {
1850 final Element pubsub = response.findChild("pubsub", Namespace.PUBSUB);
1851 final Map<Jid, Bookmark> bookmarks = Bookmark.parseFromPubsub(pubsub, account);
1852 processBookmarksInitial(account, bookmarks, true);
1853 }
1854 }
1855 });
1856 }
1857
1858 public void processBookmarksInitial(Account account, Map<Jid, Bookmark> bookmarks, final boolean pep) {
1859 final Set<Jid> previousBookmarks = account.getBookmarkedJids();
1860 final boolean synchronizeWithBookmarks = synchronizeWithBookmarks();
1861 for (Bookmark bookmark : bookmarks.values()) {
1862 previousBookmarks.remove(bookmark.getJid().asBareJid());
1863 processModifiedBookmark(bookmark, pep, synchronizeWithBookmarks);
1864 }
1865 if (pep && synchronizeWithBookmarks) {
1866 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": " + previousBookmarks.size() + " bookmarks have been removed");
1867 for (Jid jid : previousBookmarks) {
1868 processDeletedBookmark(account, jid);
1869 }
1870 }
1871 account.setBookmarks(bookmarks);
1872 }
1873
1874 public void processDeletedBookmark(Account account, Jid jid) {
1875 final Conversation conversation = find(account, jid);
1876 if (conversation != null && conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) {
1877 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": archiving destroyed conference (" + conversation.getJid() + ") after receiving pep");
1878 archiveConversation(conversation, false);
1879 }
1880 }
1881
1882 private void processModifiedBookmark(Bookmark bookmark, final boolean pep, final boolean synchronizeWithBookmarks) {
1883 final Account account = bookmark.getAccount();
1884 Conversation conversation = find(bookmark);
1885 if (conversation != null) {
1886 if (conversation.getMode() != Conversation.MODE_MULTI) {
1887 return;
1888 }
1889 bookmark.setConversation(conversation);
1890 if (pep && synchronizeWithBookmarks && !bookmark.autojoin()) {
1891 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": archiving conference (" + conversation.getJid() + ") after receiving pep");
1892 archiveConversation(conversation, false);
1893 } else {
1894 final MucOptions mucOptions = conversation.getMucOptions();
1895 if (mucOptions.getError() == MucOptions.Error.NICK_IN_USE) {
1896 final String current = mucOptions.getActualNick();
1897 final String proposed = mucOptions.getProposedNick();
1898 if (current != null && !current.equals(proposed)) {
1899 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": proposed nick changed after bookmark push " + current + "->" + proposed);
1900 joinMuc(conversation);
1901 }
1902 }
1903 }
1904 } else if (synchronizeWithBookmarks && bookmark.autojoin()) {
1905 conversation = findOrCreateConversation(account, bookmark.getFullJid(), true, true, false);
1906 bookmark.setConversation(conversation);
1907 }
1908 }
1909
1910 public void processModifiedBookmark(Bookmark bookmark) {
1911 final boolean synchronizeWithBookmarks = synchronizeWithBookmarks();
1912 processModifiedBookmark(bookmark, true, synchronizeWithBookmarks);
1913 }
1914
1915 public void createBookmark(final Account account, final Bookmark bookmark) {
1916 account.putBookmark(bookmark);
1917 final XmppConnection connection = account.getXmppConnection();
1918 if (connection == null) {
1919 Log.d(Config.LOGTAG, account.getJid().asBareJid()+": no connection. ignoring bookmark creation");
1920 } else if (connection.getFeatures().bookmarks2()) {
1921 final Element item = mIqGenerator.publishBookmarkItem(bookmark);
1922 pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS2, item, bookmark.getJid().asBareJid().toEscapedString(), PublishOptions.persistentWhitelistAccessMaxItems());
1923 } else if (connection.getFeatures().bookmarksConversion()) {
1924 pushBookmarksPep(account);
1925 } else {
1926 pushBookmarksPrivateXml(account);
1927 }
1928 }
1929
1930 public void deleteBookmark(final Account account, final Bookmark bookmark) {
1931 if (bookmark.getJid().toString().equals("discuss@conference.soprani.ca")) {
1932 getPreferences().edit().putBoolean("cheogram_sopranica_bookmark_deleted", true).apply();
1933 }
1934 account.removeBookmark(bookmark);
1935 final XmppConnection connection = account.getXmppConnection();
1936 if (connection == null) return;
1937
1938 if (connection.getFeatures().bookmarks2()) {
1939 IqPacket request = mIqGenerator.deleteItem(Namespace.BOOKMARKS2, bookmark.getJid().asBareJid().toEscapedString());
1940 sendIqPacket(account, request, (a, response) -> {
1941 if (response.getType() == IqPacket.TYPE.ERROR) {
1942 Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": unable to delete bookmark " + response.getErrorCondition());
1943 }
1944 });
1945 } else if (connection.getFeatures().bookmarksConversion()) {
1946 pushBookmarksPep(account);
1947 } else {
1948 pushBookmarksPrivateXml(account);
1949 }
1950 }
1951
1952 private void pushBookmarksPrivateXml(Account account) {
1953 if (!account.areBookmarksLoaded()) return;
1954
1955 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via private xml");
1956 IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
1957 Element query = iqPacket.query("jabber:iq:private");
1958 Element storage = query.addChild("storage", "storage:bookmarks");
1959 for (final Bookmark bookmark : account.getBookmarks()) {
1960 storage.addChild(bookmark);
1961 }
1962 sendIqPacket(account, iqPacket, mDefaultIqHandler);
1963 }
1964
1965 private void pushBookmarksPep(Account account) {
1966 if (!account.areBookmarksLoaded()) return;
1967
1968 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": pushing bookmarks via pep");
1969 final Element storage = new Element("storage", "storage:bookmarks");
1970 for (final Bookmark bookmark : account.getBookmarks()) {
1971 storage.addChild(bookmark);
1972 }
1973 pushNodeAndEnforcePublishOptions(account, Namespace.BOOKMARKS, storage, "current", PublishOptions.persistentWhitelistAccess());
1974
1975 }
1976
1977 private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final String id, final Bundle options) {
1978 pushNodeAndEnforcePublishOptions(account, node, element, id, options, true);
1979
1980 }
1981
1982 private void pushNodeAndEnforcePublishOptions(final Account account, final String node, final Element element, final String id, final Bundle options, final boolean retry) {
1983 final IqPacket packet = mIqGenerator.publishElement(node, element, id, options);
1984 sendIqPacket(account, packet, (a, response) -> {
1985 if (response.getType() == IqPacket.TYPE.RESULT) {
1986 return;
1987 }
1988 if (retry && PublishOptions.preconditionNotMet(response)) {
1989 pushNodeConfiguration(account, node, options, new OnConfigurationPushed() {
1990 @Override
1991 public void onPushSucceeded() {
1992 pushNodeAndEnforcePublishOptions(account, node, element, id, options, false);
1993 }
1994
1995 @Override
1996 public void onPushFailed() {
1997 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to push node configuration (" + node + ")");
1998 }
1999 });
2000 } else {
2001 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": error publishing bookmarks (retry=" + retry + ") " + response);
2002 }
2003 });
2004 }
2005
2006 private void restoreFromDatabase() {
2007 synchronized (this.conversations) {
2008 final Map<String, Account> accountLookupTable = new Hashtable<>();
2009 for (Account account : this.accounts) {
2010 accountLookupTable.put(account.getUuid(), account);
2011 }
2012 Log.d(Config.LOGTAG, "restoring conversations...");
2013 final long startTimeConversationsRestore = SystemClock.elapsedRealtime();
2014 this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
2015 for (Iterator<Conversation> iterator = conversations.listIterator(); iterator.hasNext(); ) {
2016 Conversation conversation = iterator.next();
2017 Account account = accountLookupTable.get(conversation.getAccountUuid());
2018 if (account != null) {
2019 conversation.setAccount(account);
2020 } else {
2021 Log.e(Config.LOGTAG, "unable to restore Conversations with " + conversation.getJid());
2022 iterator.remove();
2023 }
2024 }
2025 long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore;
2026 Log.d(Config.LOGTAG, "finished restoring conversations in " + diffConversationsRestore + "ms");
2027 Runnable runnable = () -> {
2028 if (DatabaseBackend.requiresMessageIndexRebuild()) {
2029 DatabaseBackend.getInstance(this).rebuildMessagesIndex();
2030 }
2031 final long deletionDate = getAutomaticMessageDeletionDate();
2032 mLastExpiryRun.set(SystemClock.elapsedRealtime());
2033 if (deletionDate > 0) {
2034 Log.d(Config.LOGTAG, "deleting messages that are older than " + AbstractGenerator.getTimestamp(deletionDate));
2035 databaseBackend.expireOldMessages(deletionDate);
2036 }
2037 Log.d(Config.LOGTAG, "restoring roster...");
2038 for (final Account account : accounts) {
2039 databaseBackend.readRoster(account.getRoster());
2040 account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage
2041 }
2042 getBitmapCache().evictAll();
2043 loadPhoneContacts();
2044 Log.d(Config.LOGTAG, "restoring messages...");
2045 final long startMessageRestore = SystemClock.elapsedRealtime();
2046 final Conversation quickLoad = QuickLoader.get(this.conversations);
2047 if (quickLoad != null) {
2048 restoreMessages(quickLoad);
2049 updateConversationUi();
2050 final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
2051 Log.d(Config.LOGTAG, "quickly restored " + quickLoad.getName() + " after " + diffMessageRestore + "ms");
2052 }
2053 for (Conversation conversation : this.conversations) {
2054 if (quickLoad != conversation) {
2055 restoreMessages(conversation);
2056 }
2057 }
2058 mNotificationService.finishBacklog();
2059 restoredFromDatabaseLatch.countDown();
2060 final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
2061 Log.d(Config.LOGTAG, "finished restoring messages in " + diffMessageRestore + "ms");
2062 updateConversationUi();
2063 };
2064 mDatabaseReaderExecutor.execute(runnable); //will contain one write command (expiry) but that's fine
2065 }
2066 }
2067
2068 private void restoreMessages(Conversation conversation) {
2069 conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
2070 conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
2071 conversation.findUnreadMessagesAndCalls(mNotificationService::pushFromBacklog);
2072 }
2073
2074 public void loadPhoneContacts() {
2075 mContactMergerExecutor.execute(() -> {
2076 final Map<Jid, JabberIdContact> contacts = JabberIdContact.load(this);
2077 Log.d(Config.LOGTAG, "start merging phone contacts with roster");
2078 for (final Account account : accounts) {
2079 final List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts(JabberIdContact.class);
2080 for (final JabberIdContact jidContact : contacts.values()) {
2081 final Contact contact = account.getRoster().getContact(jidContact.getJid());
2082 boolean needsCacheClean = contact.setPhoneContact(jidContact);
2083 if (needsCacheClean) {
2084 getAvatarService().clear(contact);
2085 }
2086 withSystemAccounts.remove(contact);
2087 }
2088 for (final Contact contact : withSystemAccounts) {
2089 boolean needsCacheClean = contact.unsetPhoneContact(JabberIdContact.class);
2090 if (needsCacheClean) {
2091 getAvatarService().clear(contact);
2092 }
2093 }
2094 }
2095 Log.d(Config.LOGTAG, "finished merging phone contacts");
2096 mShortcutService.refresh(mInitialAddressbookSyncCompleted.compareAndSet(false, true));
2097 updateRosterUi();
2098 mQuickConversationsService.considerSync();
2099 });
2100 }
2101
2102
2103 public void syncRoster(final Account account) {
2104 mRosterSyncTaskManager.execute(account, () -> {
2105 unregisterPhoneAccounts(account);
2106 databaseBackend.writeRoster(account.getRoster());
2107 try { Thread.sleep(500); } catch (InterruptedException e) { }
2108 });
2109 }
2110
2111 public List<Conversation> getConversations() {
2112 return this.conversations;
2113 }
2114
2115 private void markFileDeleted(final File file) {
2116 synchronized (FILENAMES_TO_IGNORE_DELETION) {
2117 if (FILENAMES_TO_IGNORE_DELETION.remove(file.getAbsolutePath())) {
2118 Log.d(Config.LOGTAG, "ignored deletion of " + file.getAbsolutePath());
2119 return;
2120 }
2121 }
2122 final boolean isInternalFile = fileBackend.isInternalFile(file);
2123 final List<String> uuids = databaseBackend.markFileAsDeleted(file, isInternalFile);
2124 Log.d(Config.LOGTAG, "deleted file " + file.getAbsolutePath() + " internal=" + isInternalFile + ", database hits=" + uuids.size());
2125 markUuidsAsDeletedFiles(uuids);
2126 }
2127
2128 private void markUuidsAsDeletedFiles(List<String> uuids) {
2129 boolean deleted = false;
2130 for (Conversation conversation : getConversations()) {
2131 deleted |= conversation.markAsDeleted(uuids);
2132 }
2133 for (final String uuid : uuids) {
2134 evictPreview(uuid);
2135 }
2136 if (deleted) {
2137 updateConversationUi();
2138 }
2139 }
2140
2141 private void markChangedFiles(List<DatabaseBackend.FilePathInfo> infos) {
2142 boolean changed = false;
2143 for (Conversation conversation : getConversations()) {
2144 changed |= conversation.markAsChanged(infos);
2145 }
2146 if (changed) {
2147 updateConversationUi();
2148 }
2149 }
2150
2151 public void populateWithOrderedConversations(final List<Conversation> list) {
2152 populateWithOrderedConversations(list, true, true);
2153 }
2154
2155 public void populateWithOrderedConversations(final List<Conversation> list, final boolean includeNoFileUpload) {
2156 populateWithOrderedConversations(list, includeNoFileUpload, true);
2157 }
2158
2159 public void populateWithOrderedConversations(final List<Conversation> list, final boolean includeNoFileUpload, final boolean sort) {
2160 final List<String> orderedUuids;
2161 if (sort) {
2162 orderedUuids = null;
2163 } else {
2164 orderedUuids = new ArrayList<>();
2165 for (Conversation conversation : list) {
2166 orderedUuids.add(conversation.getUuid());
2167 }
2168 }
2169 list.clear();
2170 if (includeNoFileUpload) {
2171 list.addAll(getConversations());
2172 } else {
2173 for (Conversation conversation : getConversations()) {
2174 if (conversation.getMode() == Conversation.MODE_SINGLE
2175 || (conversation.getAccount().httpUploadAvailable() && conversation.getMucOptions().participating())) {
2176 list.add(conversation);
2177 }
2178 }
2179 }
2180 try {
2181 if (orderedUuids != null) {
2182 Collections.sort(list, (a, b) -> {
2183 final int indexA = orderedUuids.indexOf(a.getUuid());
2184 final int indexB = orderedUuids.indexOf(b.getUuid());
2185 if (indexA == -1 || indexB == -1 || indexA == indexB) {
2186 return a.compareTo(b);
2187 }
2188 return indexA - indexB;
2189 });
2190 } else {
2191 Collections.sort(list);
2192 }
2193 } catch (IllegalArgumentException e) {
2194 //ignore
2195 }
2196 }
2197
2198 public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
2199 if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback)) {
2200 return;
2201 } else if (timestamp == 0) {
2202 return;
2203 }
2204 Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp));
2205 final Runnable runnable = () -> {
2206 final Account account = conversation.getAccount();
2207 List<Message> messages = databaseBackend.getMessages(conversation, 50, timestamp);
2208 if (messages.size() > 0) {
2209 conversation.addAll(0, messages);
2210 callback.onMoreMessagesLoaded(messages.size(), conversation);
2211 } else if (conversation.hasMessagesLeftOnServer()
2212 && account.isOnlineAndConnected()
2213 && conversation.getLastClearHistory().getTimestamp() == 0) {
2214 final boolean mamAvailable;
2215 if (conversation.getMode() == Conversation.MODE_SINGLE) {
2216 mamAvailable = account.getXmppConnection().getFeatures().mam() && !conversation.getContact().isBlocked();
2217 } else {
2218 mamAvailable = conversation.getMucOptions().mamSupport();
2219 }
2220 if (mamAvailable) {
2221 MessageArchiveService.Query query = getMessageArchiveService().query(conversation, new MamReference(0), timestamp, false);
2222 if (query != null) {
2223 query.setCallback(callback);
2224 callback.informUser(R.string.fetching_history_from_server);
2225 } else {
2226 callback.informUser(R.string.not_fetching_history_retention_period);
2227 }
2228
2229 }
2230 }
2231 };
2232 mDatabaseReaderExecutor.execute(runnable);
2233 }
2234
2235 public List<Account> getAccounts() {
2236 return this.accounts;
2237 }
2238
2239
2240 /**
2241 * This will find all conferences with the contact as member and also the conference that is the contact (that 'fake' contact is used to store the avatar)
2242 */
2243 public List<Conversation> findAllConferencesWith(Contact contact) {
2244 final ArrayList<Conversation> results = new ArrayList<>();
2245 for (final Conversation c : conversations) {
2246 if (c.getMode() != Conversation.MODE_MULTI) {
2247 continue;
2248 }
2249 final MucOptions mucOptions = c.getMucOptions();
2250 if (c.getJid().asBareJid().equals(contact.getJid().asBareJid()) || (mucOptions != null && mucOptions.isContactInRoom(contact))) {
2251 results.add(c);
2252 }
2253 }
2254 return results;
2255 }
2256
2257 public Conversation find(final Iterable<Conversation> haystack, final Contact contact) {
2258 for (final Conversation conversation : haystack) {
2259 if (conversation.getContact() == contact) {
2260 return conversation;
2261 }
2262 }
2263 return null;
2264 }
2265
2266 public Conversation find(final Iterable<Conversation> haystack, final Account account, final Jid jid) {
2267 if (jid == null) {
2268 return null;
2269 }
2270 for (final Conversation conversation : haystack) {
2271 if ((account == null || conversation.getAccount() == account)
2272 && (conversation.getJid().asBareJid().equals(jid.asBareJid()))) {
2273 return conversation;
2274 }
2275 }
2276 return null;
2277 }
2278
2279 public boolean isConversationsListEmpty(final Conversation ignore) {
2280 synchronized (this.conversations) {
2281 final int size = this.conversations.size();
2282 return size == 0 || size == 1 && this.conversations.get(0) == ignore;
2283 }
2284 }
2285
2286 public boolean isConversationStillOpen(final Conversation conversation) {
2287 synchronized (this.conversations) {
2288 for (Conversation current : this.conversations) {
2289 if (current == conversation) {
2290 return true;
2291 }
2292 }
2293 }
2294 return false;
2295 }
2296
2297 public Conversation findOrCreateConversation(Account account, Jid jid, boolean muc, final boolean async) {
2298 return this.findOrCreateConversation(account, jid, muc, false, async);
2299 }
2300
2301 public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final boolean async) {
2302 return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, null, async);
2303 }
2304
2305 public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final MessageArchiveService.Query query, final boolean async) {
2306 synchronized (this.conversations) {
2307 Conversation conversation = find(account, jid);
2308 if (conversation != null) {
2309 return conversation;
2310 }
2311 conversation = databaseBackend.findConversation(account, jid);
2312 final boolean loadMessagesFromDb;
2313 if (conversation != null) {
2314 conversation.setStatus(Conversation.STATUS_AVAILABLE);
2315 conversation.setAccount(account);
2316 if (muc) {
2317 conversation.setMode(Conversation.MODE_MULTI);
2318 conversation.setContactJid(jid);
2319 } else {
2320 conversation.setMode(Conversation.MODE_SINGLE);
2321 conversation.setContactJid(jid.asBareJid());
2322 }
2323 databaseBackend.updateConversation(conversation);
2324 loadMessagesFromDb = conversation.messagesLoaded.compareAndSet(true, false);
2325 } else {
2326 String conversationName;
2327 Contact contact = account.getRoster().getContact(jid);
2328 if (contact != null) {
2329 conversationName = contact.getDisplayName();
2330 } else {
2331 conversationName = jid.getLocal();
2332 }
2333 if (muc) {
2334 conversation = new Conversation(conversationName, account, jid,
2335 Conversation.MODE_MULTI);
2336 } else {
2337 conversation = new Conversation(conversationName, account, jid.asBareJid(),
2338 Conversation.MODE_SINGLE);
2339 }
2340 this.databaseBackend.createConversation(conversation);
2341 loadMessagesFromDb = false;
2342 }
2343 final Conversation c = conversation;
2344 final Runnable runnable = () -> {
2345 if (loadMessagesFromDb) {
2346 c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE));
2347 updateConversationUi();
2348 c.messagesLoaded.set(true);
2349 }
2350 if (account.getXmppConnection() != null
2351 && !c.getContact().isBlocked()
2352 && account.getXmppConnection().getFeatures().mam()
2353 && !muc) {
2354 if (query == null) {
2355 mMessageArchiveService.query(c);
2356 } else {
2357 if (query.getConversation() == null) {
2358 mMessageArchiveService.query(c, query.getStart(), query.isCatchup());
2359 }
2360 }
2361 }
2362 if (joinAfterCreate) {
2363 joinMuc(c);
2364 }
2365 };
2366 if (async) {
2367 mDatabaseReaderExecutor.execute(runnable);
2368 } else {
2369 runnable.run();
2370 }
2371 this.conversations.add(conversation);
2372 updateConversationUi();
2373 return conversation;
2374 }
2375 }
2376
2377 public void archiveConversation(Conversation conversation) {
2378 archiveConversation(conversation, true);
2379 }
2380
2381 private void archiveConversation(Conversation conversation, final boolean maySynchronizeWithBookmarks) {
2382 getNotificationService().clear(conversation);
2383 conversation.setStatus(Conversation.STATUS_ARCHIVED);
2384 conversation.setNextMessage(null);
2385 synchronized (this.conversations) {
2386 getMessageArchiveService().kill(conversation);
2387 if (conversation.getMode() == Conversation.MODE_MULTI) {
2388 if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
2389 final Bookmark bookmark = conversation.getBookmark();
2390 if (maySynchronizeWithBookmarks && bookmark != null && synchronizeWithBookmarks()) {
2391 if (conversation.getMucOptions().getError() == MucOptions.Error.DESTROYED) {
2392 Account account = bookmark.getAccount();
2393 bookmark.setConversation(null);
2394 deleteBookmark(account, bookmark);
2395 } else if (bookmark.autojoin()) {
2396 bookmark.setAutojoin(false);
2397 createBookmark(bookmark.getAccount(), bookmark);
2398 }
2399 }
2400 }
2401 leaveMuc(conversation);
2402 } else {
2403 if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
2404 stopPresenceUpdatesTo(conversation.getContact());
2405 }
2406 }
2407 updateConversation(conversation);
2408 this.conversations.remove(conversation);
2409 updateConversationUi();
2410 }
2411 }
2412
2413 public void stopPresenceUpdatesTo(Contact contact) {
2414 Log.d(Config.LOGTAG, "Canceling presence request from " + contact.getJid().toString());
2415 sendPresencePacket(contact.getAccount(), mPresenceGenerator.stopPresenceUpdatesTo(contact));
2416 contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
2417 }
2418
2419 public void createAccount(final Account account) {
2420 account.initAccountServices(this);
2421 databaseBackend.createAccount(account);
2422 this.accounts.add(account);
2423 this.reconnectAccountInBackground(account);
2424 updateAccountUi();
2425 syncEnabledAccountSetting();
2426 toggleForegroundService();
2427 }
2428
2429 private void syncEnabledAccountSetting() {
2430 final boolean hasEnabledAccounts = hasEnabledAccounts();
2431 getPreferences().edit().putBoolean(EventReceiver.SETTING_ENABLED_ACCOUNTS, hasEnabledAccounts).apply();
2432 toggleSetProfilePictureActivity(hasEnabledAccounts);
2433 }
2434
2435 private void toggleSetProfilePictureActivity(final boolean enabled) {
2436 try {
2437 final ComponentName name = new ComponentName(this, ChooseAccountForProfilePictureActivity.class);
2438 final int targetState = enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
2439 getPackageManager().setComponentEnabledSetting(name, targetState, PackageManager.DONT_KILL_APP);
2440 } catch (IllegalStateException e) {
2441 Log.d(Config.LOGTAG, "unable to toggle profile picture activity");
2442 }
2443 }
2444
2445 public boolean reconfigurePushDistributor() {
2446 return this.unifiedPushBroker.reconfigurePushDistributor();
2447 }
2448
2449 public Optional<UnifiedPushBroker.Transport> renewUnifiedPushEndpoints() {
2450 return this.unifiedPushBroker.renewUnifiedPushEndpoints();
2451 }
2452
2453 private void provisionAccount(final String address, final String password) {
2454 final Jid jid = Jid.ofEscaped(address);
2455 final Account account = new Account(jid, password);
2456 account.setOption(Account.OPTION_DISABLED, true);
2457 Log.d(Config.LOGTAG, jid.asBareJid().toEscapedString() + ": provisioning account");
2458 createAccount(account);
2459 }
2460
2461 public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
2462 new Thread(() -> {
2463 try {
2464 final X509Certificate[] chain = KeyChain.getCertificateChain(this, alias);
2465 final X509Certificate cert = chain != null && chain.length > 0 ? chain[0] : null;
2466 if (cert == null) {
2467 callback.informUser(R.string.unable_to_parse_certificate);
2468 return;
2469 }
2470 Pair<Jid, String> info = CryptoHelper.extractJidAndName(cert);
2471 if (info == null) {
2472 callback.informUser(R.string.certificate_does_not_contain_jid);
2473 return;
2474 }
2475 if (findAccountByJid(info.first) == null) {
2476 final Account account = new Account(info.first, "");
2477 account.setPrivateKeyAlias(alias);
2478 account.setOption(Account.OPTION_DISABLED, true);
2479 account.setOption(Account.OPTION_FIXED_USERNAME, true);
2480 account.setDisplayName(info.second);
2481 createAccount(account);
2482 callback.onAccountCreated(account);
2483 if (Config.X509_VERIFICATION) {
2484 try {
2485 getMemorizingTrustManager().getNonInteractive(account.getServer()).checkClientTrusted(chain, "RSA");
2486 } catch (CertificateException e) {
2487 callback.informUser(R.string.certificate_chain_is_not_trusted);
2488 }
2489 }
2490 } else {
2491 callback.informUser(R.string.account_already_exists);
2492 }
2493 } catch (Exception e) {
2494 callback.informUser(R.string.unable_to_parse_certificate);
2495 }
2496 }).start();
2497
2498 }
2499
2500 public void updateKeyInAccount(final Account account, final String alias) {
2501 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": update key in account " + alias);
2502 try {
2503 X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
2504 Log.d(Config.LOGTAG, account.getJid().asBareJid() + " loaded certificate chain");
2505 Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
2506 if (info == null) {
2507 showErrorToastInUi(R.string.certificate_does_not_contain_jid);
2508 return;
2509 }
2510 if (account.getJid().asBareJid().equals(info.first)) {
2511 account.setPrivateKeyAlias(alias);
2512 account.setDisplayName(info.second);
2513 databaseBackend.updateAccount(account);
2514 if (Config.X509_VERIFICATION) {
2515 try {
2516 getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
2517 } catch (CertificateException e) {
2518 showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
2519 }
2520 account.getAxolotlService().regenerateKeys(true);
2521 }
2522 } else {
2523 showErrorToastInUi(R.string.jid_does_not_match_certificate);
2524 }
2525 } catch (Exception e) {
2526 e.printStackTrace();
2527 }
2528 }
2529
2530 public boolean updateAccount(final Account account) {
2531 if (databaseBackend.updateAccount(account)) {
2532 account.setShowErrorNotification(true);
2533 this.statusListener.onStatusChanged(account);
2534 databaseBackend.updateAccount(account);
2535 reconnectAccountInBackground(account);
2536 updateAccountUi();
2537 getNotificationService().updateErrorNotification();
2538 toggleForegroundService();
2539 syncEnabledAccountSetting();
2540 mChannelDiscoveryService.cleanCache();
2541 return true;
2542 } else {
2543 return false;
2544 }
2545 }
2546
2547 public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) {
2548 final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword);
2549 sendIqPacket(account, iq, (a, packet) -> {
2550 if (packet.getType() == IqPacket.TYPE.RESULT) {
2551 a.setPassword(newPassword);
2552 a.setOption(Account.OPTION_MAGIC_CREATE, false);
2553 databaseBackend.updateAccount(a);
2554 callback.onPasswordChangeSucceeded();
2555 } else {
2556 callback.onPasswordChangeFailed();
2557 }
2558 });
2559 }
2560
2561 public void deleteAccount(final Account account) {
2562 final boolean connected = account.getStatus() == Account.State.ONLINE;
2563 synchronized (this.conversations) {
2564 if (connected) {
2565 account.getAxolotlService().deleteOmemoIdentity();
2566 }
2567 for (final Conversation conversation : conversations) {
2568 if (conversation.getAccount() == account) {
2569 if (conversation.getMode() == Conversation.MODE_MULTI) {
2570 if (connected) {
2571 leaveMuc(conversation);
2572 }
2573 }
2574 conversations.remove(conversation);
2575 mNotificationService.clear(conversation);
2576 }
2577 }
2578 new Thread(() -> {
2579 for (final Contact contact : account.getRoster().getContacts()) {
2580 contact.unregisterAsPhoneAccount(this);
2581 }
2582 }).start();
2583 if (account.getXmppConnection() != null) {
2584 new Thread(() -> disconnect(account, !connected)).start();
2585 }
2586 final Runnable runnable = () -> {
2587 if (!databaseBackend.deleteAccount(account)) {
2588 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to delete account");
2589 }
2590 };
2591 mDatabaseWriterExecutor.execute(runnable);
2592 this.accounts.remove(account);
2593 this.mRosterSyncTaskManager.clear(account);
2594 updateAccountUi();
2595 mNotificationService.updateErrorNotification();
2596 syncEnabledAccountSetting();
2597 toggleForegroundService();
2598 }
2599 }
2600
2601 public void setOnConversationListChangedListener(OnConversationUpdate listener) {
2602 final boolean remainingListeners;
2603 synchronized (LISTENER_LOCK) {
2604 remainingListeners = checkListeners();
2605 if (!this.mOnConversationUpdates.add(listener)) {
2606 Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as ConversationListChangedListener");
2607 }
2608 this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
2609 }
2610 if (remainingListeners) {
2611 switchToForeground();
2612 }
2613 }
2614
2615 public void removeOnConversationListChangedListener(OnConversationUpdate listener) {
2616 final boolean remainingListeners;
2617 synchronized (LISTENER_LOCK) {
2618 this.mOnConversationUpdates.remove(listener);
2619 this.mNotificationService.setIsInForeground(this.mOnConversationUpdates.size() > 0);
2620 remainingListeners = checkListeners();
2621 }
2622 if (remainingListeners) {
2623 switchToBackground();
2624 }
2625 }
2626
2627 public void setOnShowErrorToastListener(OnShowErrorToast listener) {
2628 final boolean remainingListeners;
2629 synchronized (LISTENER_LOCK) {
2630 remainingListeners = checkListeners();
2631 if (!this.mOnShowErrorToasts.add(listener)) {
2632 Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnShowErrorToastListener");
2633 }
2634 }
2635 if (remainingListeners) {
2636 switchToForeground();
2637 }
2638 }
2639
2640 public void removeOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
2641 final boolean remainingListeners;
2642 synchronized (LISTENER_LOCK) {
2643 this.mOnShowErrorToasts.remove(onShowErrorToast);
2644 remainingListeners = checkListeners();
2645 }
2646 if (remainingListeners) {
2647 switchToBackground();
2648 }
2649 }
2650
2651 public void setOnAccountListChangedListener(OnAccountUpdate listener) {
2652 final boolean remainingListeners;
2653 synchronized (LISTENER_LOCK) {
2654 remainingListeners = checkListeners();
2655 if (!this.mOnAccountUpdates.add(listener)) {
2656 Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnAccountListChangedtListener");
2657 }
2658 }
2659 if (remainingListeners) {
2660 switchToForeground();
2661 }
2662 }
2663
2664 public void removeOnAccountListChangedListener(OnAccountUpdate listener) {
2665 final boolean remainingListeners;
2666 synchronized (LISTENER_LOCK) {
2667 this.mOnAccountUpdates.remove(listener);
2668 remainingListeners = checkListeners();
2669 }
2670 if (remainingListeners) {
2671 switchToBackground();
2672 }
2673 }
2674
2675 public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
2676 final boolean remainingListeners;
2677 synchronized (LISTENER_LOCK) {
2678 remainingListeners = checkListeners();
2679 if (!this.mOnCaptchaRequested.add(listener)) {
2680 Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnCaptchaRequestListener");
2681 }
2682 }
2683 if (remainingListeners) {
2684 switchToForeground();
2685 }
2686 }
2687
2688 public void removeOnCaptchaRequestedListener(OnCaptchaRequested listener) {
2689 final boolean remainingListeners;
2690 synchronized (LISTENER_LOCK) {
2691 this.mOnCaptchaRequested.remove(listener);
2692 remainingListeners = checkListeners();
2693 }
2694 if (remainingListeners) {
2695 switchToBackground();
2696 }
2697 }
2698
2699 public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
2700 final boolean remainingListeners;
2701 synchronized (LISTENER_LOCK) {
2702 remainingListeners = checkListeners();
2703 if (!this.mOnRosterUpdates.add(listener)) {
2704 Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnRosterUpdateListener");
2705 }
2706 }
2707 if (remainingListeners) {
2708 switchToForeground();
2709 }
2710 }
2711
2712 public void removeOnRosterUpdateListener(final OnRosterUpdate listener) {
2713 final boolean remainingListeners;
2714 synchronized (LISTENER_LOCK) {
2715 this.mOnRosterUpdates.remove(listener);
2716 remainingListeners = checkListeners();
2717 }
2718 if (remainingListeners) {
2719 switchToBackground();
2720 }
2721 }
2722
2723 public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
2724 final boolean remainingListeners;
2725 synchronized (LISTENER_LOCK) {
2726 remainingListeners = checkListeners();
2727 if (!this.mOnUpdateBlocklist.add(listener)) {
2728 Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnUpdateBlocklistListener");
2729 }
2730 }
2731 if (remainingListeners) {
2732 switchToForeground();
2733 }
2734 }
2735
2736 public void removeOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
2737 final boolean remainingListeners;
2738 synchronized (LISTENER_LOCK) {
2739 this.mOnUpdateBlocklist.remove(listener);
2740 remainingListeners = checkListeners();
2741 }
2742 if (remainingListeners) {
2743 switchToBackground();
2744 }
2745 }
2746
2747 public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
2748 final boolean remainingListeners;
2749 synchronized (LISTENER_LOCK) {
2750 remainingListeners = checkListeners();
2751 if (!this.mOnKeyStatusUpdated.add(listener)) {
2752 Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnKeyStatusUpdateListener");
2753 }
2754 }
2755 if (remainingListeners) {
2756 switchToForeground();
2757 }
2758 }
2759
2760 public void removeOnNewKeysAvailableListener(final OnKeyStatusUpdated listener) {
2761 final boolean remainingListeners;
2762 synchronized (LISTENER_LOCK) {
2763 this.mOnKeyStatusUpdated.remove(listener);
2764 remainingListeners = checkListeners();
2765 }
2766 if (remainingListeners) {
2767 switchToBackground();
2768 }
2769 }
2770
2771 public void setOnRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
2772 final boolean remainingListeners;
2773 synchronized (LISTENER_LOCK) {
2774 remainingListeners = checkListeners();
2775 if (!this.onJingleRtpConnectionUpdate.add(listener)) {
2776 Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnJingleRtpConnectionUpdate");
2777 }
2778 }
2779 if (remainingListeners) {
2780 switchToForeground();
2781 }
2782 }
2783
2784 public void removeRtpConnectionUpdateListener(final OnJingleRtpConnectionUpdate listener) {
2785 final boolean remainingListeners;
2786 synchronized (LISTENER_LOCK) {
2787 this.onJingleRtpConnectionUpdate.remove(listener);
2788 remainingListeners = checkListeners();
2789 }
2790 if (remainingListeners) {
2791 switchToBackground();
2792 }
2793 }
2794
2795 public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
2796 final boolean remainingListeners;
2797 synchronized (LISTENER_LOCK) {
2798 remainingListeners = checkListeners();
2799 if (!this.mOnMucRosterUpdate.add(listener)) {
2800 Log.w(Config.LOGTAG, listener.getClass().getName() + " is already registered as OnMucRosterListener");
2801 }
2802 }
2803 if (remainingListeners) {
2804 switchToForeground();
2805 }
2806 }
2807
2808 public void removeOnMucRosterUpdateListener(final OnMucRosterUpdate listener) {
2809 final boolean remainingListeners;
2810 synchronized (LISTENER_LOCK) {
2811 this.mOnMucRosterUpdate.remove(listener);
2812 remainingListeners = checkListeners();
2813 }
2814 if (remainingListeners) {
2815 switchToBackground();
2816 }
2817 }
2818
2819 public boolean checkListeners() {
2820 return (this.mOnAccountUpdates.size() == 0
2821 && this.mOnConversationUpdates.size() == 0
2822 && this.mOnRosterUpdates.size() == 0
2823 && this.mOnCaptchaRequested.size() == 0
2824 && this.mOnMucRosterUpdate.size() == 0
2825 && this.mOnUpdateBlocklist.size() == 0
2826 && this.mOnShowErrorToasts.size() == 0
2827 && this.onJingleRtpConnectionUpdate.size() == 0
2828 && this.mOnKeyStatusUpdated.size() == 0);
2829 }
2830
2831 private void switchToForeground() {
2832 final boolean broadcastLastActivity = broadcastLastActivity();
2833 for (Conversation conversation : getConversations()) {
2834 if (conversation.getMode() == Conversation.MODE_MULTI) {
2835 conversation.getMucOptions().resetChatState();
2836 } else {
2837 conversation.setIncomingChatState(Config.DEFAULT_CHAT_STATE);
2838 }
2839 }
2840 for (Account account : getAccounts()) {
2841 if (account.getStatus() == Account.State.ONLINE) {
2842 account.deactivateGracePeriod();
2843 final XmppConnection connection = account.getXmppConnection();
2844 if (connection != null) {
2845 if (connection.getFeatures().csi()) {
2846 connection.sendActive();
2847 }
2848 if (broadcastLastActivity) {
2849 sendPresence(account, false); //send new presence but don't include idle because we are not
2850 }
2851 }
2852 }
2853 }
2854 Log.d(Config.LOGTAG, "app switched into foreground");
2855 }
2856
2857 private void switchToBackground() {
2858 final boolean broadcastLastActivity = broadcastLastActivity();
2859 if (broadcastLastActivity) {
2860 mLastActivity = System.currentTimeMillis();
2861 final SharedPreferences.Editor editor = getPreferences().edit();
2862 editor.putLong(SETTING_LAST_ACTIVITY_TS, mLastActivity);
2863 editor.apply();
2864 }
2865 for (Account account : getAccounts()) {
2866 if (account.getStatus() == Account.State.ONLINE) {
2867 XmppConnection connection = account.getXmppConnection();
2868 if (connection != null) {
2869 if (broadcastLastActivity) {
2870 sendPresence(account, true);
2871 }
2872 if (connection.getFeatures().csi()) {
2873 connection.sendInactive();
2874 }
2875 }
2876 }
2877 }
2878 this.mNotificationService.setIsInForeground(false);
2879 Log.d(Config.LOGTAG, "app switched into background");
2880 }
2881
2882 private void connectMultiModeConversations(Account account) {
2883 List<Conversation> conversations = getConversations();
2884 for (Conversation conversation : conversations) {
2885 if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) {
2886 joinMuc(conversation);
2887 }
2888 }
2889 }
2890
2891 public void mucSelfPingAndRejoin(final Conversation conversation) {
2892 final Account account = conversation.getAccount();
2893 synchronized (account.inProgressConferenceJoins) {
2894 if (account.inProgressConferenceJoins.contains(conversation)) {
2895 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": canceling muc self ping because join is already under way");
2896 return;
2897 }
2898 }
2899 synchronized (account.inProgressConferencePings) {
2900 if (!account.inProgressConferencePings.add(conversation)) {
2901 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": canceling muc self ping because ping is already under way");
2902 return;
2903 }
2904 }
2905 final Jid self = conversation.getMucOptions().getSelf().getFullJid();
2906 final IqPacket ping = new IqPacket(IqPacket.TYPE.GET);
2907 ping.setTo(self);
2908 ping.addChild("ping", Namespace.PING);
2909 sendIqPacket(conversation.getAccount(), ping, (a, response) -> {
2910 if (response.getType() == IqPacket.TYPE.ERROR) {
2911 Element error = response.findChild("error");
2912 if (error == null || error.hasChild("service-unavailable") || error.hasChild("feature-not-implemented") || error.hasChild("item-not-found")) {
2913 Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " came back as ignorable error");
2914 } else {
2915 Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " failed. attempting rejoin");
2916 joinMuc(conversation);
2917 }
2918 } else if (response.getType() == IqPacket.TYPE.RESULT) {
2919 Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": ping to " + self + " came back fine");
2920 }
2921 synchronized (account.inProgressConferencePings) {
2922 account.inProgressConferencePings.remove(conversation);
2923 }
2924 });
2925 }
2926 public void joinMuc(Conversation conversation) {
2927 joinMuc(conversation, null, false);
2928 }
2929
2930 public void joinMuc(Conversation conversation, boolean followedInvite) {
2931 joinMuc(conversation, null, followedInvite);
2932 }
2933
2934 private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
2935 joinMuc(conversation, onConferenceJoined, false);
2936 }
2937
2938 private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined, final boolean followedInvite) {
2939 final Account account = conversation.getAccount();
2940 synchronized (account.pendingConferenceJoins) {
2941 account.pendingConferenceJoins.remove(conversation);
2942 }
2943 synchronized (account.pendingConferenceLeaves) {
2944 account.pendingConferenceLeaves.remove(conversation);
2945 }
2946 if (account.getStatus() == Account.State.ONLINE) {
2947 synchronized (account.inProgressConferenceJoins) {
2948 account.inProgressConferenceJoins.add(conversation);
2949 }
2950 if (Config.MUC_LEAVE_BEFORE_JOIN) {
2951 sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions()));
2952 }
2953 conversation.resetMucOptions();
2954 if (onConferenceJoined != null) {
2955 conversation.getMucOptions().flagNoAutoPushConfiguration();
2956 }
2957 conversation.setHasMessagesLeftOnServer(false);
2958 fetchConferenceConfiguration(conversation, new OnConferenceConfigurationFetched() {
2959
2960 private void join(Conversation conversation) {
2961 Account account = conversation.getAccount();
2962 final MucOptions mucOptions = conversation.getMucOptions();
2963
2964 if (mucOptions.nonanonymous() && !mucOptions.membersOnly() && !conversation.getBooleanAttribute("accept_non_anonymous", false)) {
2965 synchronized (account.inProgressConferenceJoins) {
2966 account.inProgressConferenceJoins.remove(conversation);
2967 }
2968 mucOptions.setError(MucOptions.Error.NON_ANONYMOUS);
2969 updateConversationUi();
2970 if (onConferenceJoined != null) {
2971 onConferenceJoined.onConferenceJoined(conversation);
2972 }
2973 return;
2974 }
2975
2976 final Jid joinJid = mucOptions.getSelf().getFullJid();
2977 Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": joining conversation " + joinJid.toString());
2978 PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null);
2979 packet.setTo(joinJid);
2980 Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
2981 if (conversation.getMucOptions().getPassword() != null) {
2982 x.addChild("password").setContent(mucOptions.getPassword());
2983 }
2984
2985 if (mucOptions.mamSupport()) {
2986 // Use MAM instead of the limited muc history to get history
2987 x.addChild("history").setAttribute("maxchars", "0");
2988 } else {
2989 // Fallback to muc history
2990 x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted().getTimestamp()));
2991 }
2992 sendPresencePacket(account, packet);
2993 if (onConferenceJoined != null) {
2994 onConferenceJoined.onConferenceJoined(conversation);
2995 }
2996 if (!joinJid.equals(conversation.getJid())) {
2997 conversation.setContactJid(joinJid);
2998 databaseBackend.updateConversation(conversation);
2999 }
3000
3001 if (mucOptions.mamSupport()) {
3002 getMessageArchiveService().catchupMUC(conversation);
3003 }
3004 if (mucOptions.isPrivateAndNonAnonymous()) {
3005 fetchConferenceMembers(conversation);
3006
3007 if (followedInvite) {
3008 final Bookmark bookmark = conversation.getBookmark();
3009 if (bookmark != null) {
3010 if (!bookmark.autojoin()) {
3011 bookmark.setAutojoin(true);
3012 createBookmark(account, bookmark);
3013 }
3014 } else {
3015 saveConversationAsBookmark(conversation, null);
3016 }
3017 }
3018 }
3019 synchronized (account.inProgressConferenceJoins) {
3020 account.inProgressConferenceJoins.remove(conversation);
3021 sendUnsentMessages(conversation);
3022 }
3023 }
3024
3025 @Override
3026 public void onConferenceConfigurationFetched(Conversation conversation) {
3027 if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
3028 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": conversation (" + conversation.getJid() + ") got archived before IQ result");
3029 return;
3030 }
3031 join(conversation);
3032 }
3033
3034 @Override
3035 public void onFetchFailed(final Conversation conversation, final String errorCondition) {
3036 if (conversation.getStatus() == Conversation.STATUS_ARCHIVED) {
3037 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": conversation (" + conversation.getJid() + ") got archived before IQ result");
3038 return;
3039 }
3040 if ("remote-server-not-found".equals(errorCondition)) {
3041 synchronized (account.inProgressConferenceJoins) {
3042 account.inProgressConferenceJoins.remove(conversation);
3043 }
3044 conversation.getMucOptions().setError(MucOptions.Error.SERVER_NOT_FOUND);
3045 updateConversationUi();
3046 } else {
3047 join(conversation);
3048 fetchConferenceConfiguration(conversation);
3049 }
3050 }
3051 });
3052 updateConversationUi();
3053 } else {
3054 synchronized (account.pendingConferenceJoins) {
3055 account.pendingConferenceJoins.add(conversation);
3056 }
3057 conversation.resetMucOptions();
3058 conversation.setHasMessagesLeftOnServer(false);
3059 updateConversationUi();
3060 }
3061 }
3062
3063 private void fetchConferenceMembers(final Conversation conversation) {
3064 final Account account = conversation.getAccount();
3065 final AxolotlService axolotlService = account.getAxolotlService();
3066 final String[] affiliations = {"member", "admin", "owner"};
3067 OnIqPacketReceived callback = new OnIqPacketReceived() {
3068
3069 private int i = 0;
3070 private boolean success = true;
3071
3072 @Override
3073 public void onIqPacketReceived(Account account, IqPacket packet) {
3074 final boolean omemoEnabled = conversation.getNextEncryption() == Message.ENCRYPTION_AXOLOTL;
3075 Element query = packet.query("http://jabber.org/protocol/muc#admin");
3076 if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
3077 for (Element child : query.getChildren()) {
3078 if ("item".equals(child.getName())) {
3079 MucOptions.User user = AbstractParser.parseItem(conversation, child);
3080 if (!user.realJidMatchesAccount()) {
3081 boolean isNew = conversation.getMucOptions().updateUser(user);
3082 Contact contact = user.getContact();
3083 if (omemoEnabled
3084 && isNew
3085 && user.getRealJid() != null
3086 && (contact == null || !contact.mutualPresenceSubscription())
3087 && axolotlService.hasEmptyDeviceList(user.getRealJid())) {
3088 axolotlService.fetchDeviceIds(user.getRealJid());
3089 }
3090 }
3091 }
3092 }
3093 } else {
3094 success = false;
3095 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": could not request affiliation " + affiliations[i] + " in " + conversation.getJid().asBareJid());
3096 }
3097 ++i;
3098 if (i >= affiliations.length) {
3099 List<Jid> members = conversation.getMucOptions().getMembers(true);
3100 if (success) {
3101 List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
3102 boolean changed = false;
3103 for (ListIterator<Jid> iterator = cryptoTargets.listIterator(); iterator.hasNext(); ) {
3104 Jid jid = iterator.next();
3105 if (!members.contains(jid) && !members.contains(jid.getDomain())) {
3106 iterator.remove();
3107 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": removed " + jid + " from crypto targets of " + conversation.getName());
3108 changed = true;
3109 }
3110 }
3111 if (changed) {
3112 conversation.setAcceptedCryptoTargets(cryptoTargets);
3113 updateConversation(conversation);
3114 }
3115 }
3116 getAvatarService().clear(conversation);
3117 updateMucRosterUi();
3118 updateConversationUi();
3119 }
3120 }
3121 };
3122 for (String affiliation : affiliations) {
3123 sendIqPacket(account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
3124 }
3125 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": fetching members for " + conversation.getName());
3126 }
3127
3128 public void providePasswordForMuc(Conversation conversation, String password) {
3129 if (conversation.getMode() == Conversation.MODE_MULTI) {
3130 conversation.getMucOptions().setPassword(password);
3131 if (conversation.getBookmark() != null) {
3132 final Bookmark bookmark = conversation.getBookmark();
3133 if (synchronizeWithBookmarks()) {
3134 bookmark.setAutojoin(true);
3135 }
3136 createBookmark(conversation.getAccount(), bookmark);
3137 }
3138 updateConversation(conversation);
3139 joinMuc(conversation);
3140 }
3141 }
3142
3143 public void deleteAvatar(final Account account) {
3144 final AtomicBoolean executed = new AtomicBoolean(false);
3145 final Runnable onDeleted =
3146 () -> {
3147 if (executed.compareAndSet(false, true)) {
3148 account.setAvatar(null);
3149 databaseBackend.updateAccount(account);
3150 getAvatarService().clear(account);
3151 updateAccountUi();
3152 }
3153 };
3154 deleteVcardAvatar(account, onDeleted);
3155 deletePepNode(account, Namespace.AVATAR_DATA);
3156 deletePepNode(account, Namespace.AVATAR_METADATA, onDeleted);
3157 }
3158
3159 public void deletePepNode(final Account account, final String node) {
3160 deletePepNode(account, node, null);
3161 }
3162
3163 private void deletePepNode(final Account account, final String node, final Runnable runnable) {
3164 final IqPacket request = mIqGenerator.deleteNode(node);
3165 sendIqPacket(account, request, (a, packet) -> {
3166 if (packet.getType() == IqPacket.TYPE.RESULT) {
3167 Log.d(Config.LOGTAG,a.getJid().asBareJid()+": successfully deleted pep node "+node);
3168 if (runnable != null) {
3169 runnable.run();
3170 }
3171 } else {
3172 Log.d(Config.LOGTAG,a.getJid().asBareJid()+": failed to delete "+ packet);
3173 }
3174 });
3175 }
3176
3177 private void deleteVcardAvatar(final Account account, @NonNull final Runnable runnable) {
3178 final IqPacket retrieveVcard = mIqGenerator.retrieveVcardAvatar(account.getJid().asBareJid());
3179 sendIqPacket(account, retrieveVcard, (a, response) -> {
3180 if (response.getType() != IqPacket.TYPE.RESULT) {
3181 Log.d(Config.LOGTAG,a.getJid().asBareJid()+": no vCard set. nothing to do");
3182 return;
3183 }
3184 final Element vcard = response.findChild("vCard", "vcard-temp");
3185 if (vcard == null) {
3186 Log.d(Config.LOGTAG,a.getJid().asBareJid()+": no vCard set. nothing to do");
3187 return;
3188 }
3189 Element photo = vcard.findChild("PHOTO");
3190 if (photo == null) {
3191 photo = vcard.addChild("PHOTO");
3192 }
3193 photo.clearChildren();
3194 IqPacket publication = new IqPacket(IqPacket.TYPE.SET);
3195 publication.setTo(a.getJid().asBareJid());
3196 publication.addChild(vcard);
3197 sendIqPacket(account, publication, (a1, publicationResponse) -> {
3198 if (publicationResponse.getType() == IqPacket.TYPE.RESULT) {
3199 Log.d(Config.LOGTAG,a1.getJid().asBareJid()+": successfully deleted vcard avatar");
3200 runnable.run();
3201 } else {
3202 Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getErrorCondition());
3203 }
3204 });
3205 });
3206 }
3207
3208 private boolean hasEnabledAccounts() {
3209 if (this.accounts == null) {
3210 return false;
3211 }
3212 for (Account account : this.accounts) {
3213 if (account.isEnabled()) {
3214 return true;
3215 }
3216 }
3217 return false;
3218 }
3219
3220
3221 public void getAttachments(final Conversation conversation, int limit, final OnMediaLoaded onMediaLoaded) {
3222 getAttachments(conversation.getAccount(), conversation.getJid().asBareJid(), limit, onMediaLoaded);
3223 }
3224
3225 public void getAttachments(final Account account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) {
3226 getAttachments(account.getUuid(), jid.asBareJid(), limit, onMediaLoaded);
3227 }
3228
3229
3230 public void getAttachments(final String account, final Jid jid, final int limit, final OnMediaLoaded onMediaLoaded) {
3231 new Thread(() -> onMediaLoaded.onMediaLoaded(fileBackend.convertToAttachments(databaseBackend.getRelativeFilePaths(account, jid, limit)))).start();
3232 }
3233
3234 public void persistSelfNick(MucOptions.User self) {
3235 final Conversation conversation = self.getConversation();
3236 final boolean tookProposedNickFromBookmark = conversation.getMucOptions().isTookProposedNickFromBookmark();
3237 Jid full = self.getFullJid();
3238 if (!full.equals(conversation.getJid())) {
3239 Log.d(Config.LOGTAG, "nick changed. updating");
3240 conversation.setContactJid(full);
3241 databaseBackend.updateConversation(conversation);
3242 }
3243
3244 final Bookmark bookmark = conversation.getBookmark();
3245 final String bookmarkedNick = bookmark == null ? null : bookmark.getNick();
3246 if (bookmark != null && (tookProposedNickFromBookmark || TextUtils.isEmpty(bookmarkedNick)) && !full.getResource().equals(bookmarkedNick)) {
3247 final Account account = conversation.getAccount();
3248 final String defaultNick = MucOptions.defaultNick(account);
3249 if (TextUtils.isEmpty(bookmarkedNick) && full.getResource().equals(defaultNick)) {
3250 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": do not overwrite empty bookmark nick with default nick for " + conversation.getJid().asBareJid());
3251 return;
3252 }
3253 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": persist nick '" + full.getResource() + "' into bookmark for " + conversation.getJid().asBareJid());
3254 bookmark.setNick(full.getResource());
3255 createBookmark(bookmark.getAccount(), bookmark);
3256 }
3257 }
3258
3259 public boolean renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
3260 final MucOptions options = conversation.getMucOptions();
3261 final Jid joinJid = options.createJoinJid(nick);
3262 if (joinJid == null) {
3263 return false;
3264 }
3265 if (options.online()) {
3266 Account account = conversation.getAccount();
3267 options.setOnRenameListener(new OnRenameListener() {
3268
3269 @Override
3270 public void onSuccess() {
3271 callback.success(conversation);
3272 }
3273
3274 @Override
3275 public void onFailure() {
3276 callback.error(R.string.nick_in_use, conversation);
3277 }
3278 });
3279
3280 final PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, options.nonanonymous());
3281 packet.setTo(joinJid);
3282 sendPresencePacket(account, packet);
3283 } else {
3284 conversation.setContactJid(joinJid);
3285 databaseBackend.updateConversation(conversation);
3286 if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
3287 Bookmark bookmark = conversation.getBookmark();
3288 if (bookmark != null) {
3289 bookmark.setNick(nick);
3290 createBookmark(bookmark.getAccount(), bookmark);
3291 }
3292 joinMuc(conversation);
3293 }
3294 }
3295 return true;
3296 }
3297
3298 public void leaveMuc(Conversation conversation) {
3299 leaveMuc(conversation, false);
3300 }
3301
3302 private void leaveMuc(Conversation conversation, boolean now) {
3303 final Account account = conversation.getAccount();
3304 synchronized (account.pendingConferenceJoins) {
3305 account.pendingConferenceJoins.remove(conversation);
3306 }
3307 synchronized (account.pendingConferenceLeaves) {
3308 account.pendingConferenceLeaves.remove(conversation);
3309 }
3310 if (account.getStatus() == Account.State.ONLINE || now) {
3311 sendPresencePacket(conversation.getAccount(), mPresenceGenerator.leave(conversation.getMucOptions()));
3312 conversation.getMucOptions().setOffline();
3313 Bookmark bookmark = conversation.getBookmark();
3314 if (bookmark != null) {
3315 bookmark.setConversation(null);
3316 }
3317 Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": leaving muc " + conversation.getJid());
3318 } else {
3319 synchronized (account.pendingConferenceLeaves) {
3320 account.pendingConferenceLeaves.add(conversation);
3321 }
3322 }
3323 }
3324
3325 public String findConferenceServer(final Account account) {
3326 String server;
3327 if (account.getXmppConnection() != null) {
3328 server = account.getXmppConnection().getMucServer();
3329 if (server != null) {
3330 return server;
3331 }
3332 }
3333 for (Account other : getAccounts()) {
3334 if (other != account && other.getXmppConnection() != null) {
3335 server = other.getXmppConnection().getMucServer();
3336 if (server != null) {
3337 return server;
3338 }
3339 }
3340 }
3341 return null;
3342 }
3343
3344
3345 public void createPublicChannel(final Account account, final String name, final Jid address, final UiCallback<Conversation> callback) {
3346 joinMuc(findOrCreateConversation(account, address, true, false, true), conversation -> {
3347 final Bundle configuration = IqGenerator.defaultChannelConfiguration();
3348 if (!TextUtils.isEmpty(name)) {
3349 configuration.putString("muc#roomconfig_roomname", name);
3350 }
3351 pushConferenceConfiguration(conversation, configuration, new OnConfigurationPushed() {
3352 @Override
3353 public void onPushSucceeded() {
3354 saveConversationAsBookmark(conversation, name);
3355 callback.success(conversation);
3356 }
3357
3358 @Override
3359 public void onPushFailed() {
3360 if (conversation.getMucOptions().getSelf().getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
3361 callback.error(R.string.unable_to_set_channel_configuration, conversation);
3362 } else {
3363 callback.error(R.string.joined_an_existing_channel, conversation);
3364 }
3365 }
3366 });
3367 });
3368 }
3369
3370 public boolean createAdhocConference(final Account account,
3371 final String name,
3372 final Iterable<Jid> jids,
3373 final UiCallback<Conversation> callback) {
3374 Log.d(Config.LOGTAG, account.getJid().asBareJid().toString() + ": creating adhoc conference with " + jids.toString());
3375 if (account.getStatus() == Account.State.ONLINE) {
3376 try {
3377 String server = findConferenceServer(account);
3378 if (server == null) {
3379 if (callback != null) {
3380 callback.error(R.string.no_conference_server_found, null);
3381 }
3382 return false;
3383 }
3384 final Jid jid = Jid.of(CryptoHelper.pronounceable(), server, null);
3385 final Conversation conversation = findOrCreateConversation(account, jid, true, false, true);
3386 joinMuc(conversation, new OnConferenceJoined() {
3387 @Override
3388 public void onConferenceJoined(final Conversation conversation) {
3389 final Bundle configuration = IqGenerator.defaultGroupChatConfiguration();
3390 if (!TextUtils.isEmpty(name)) {
3391 configuration.putString("muc#roomconfig_roomname", name);
3392 }
3393 pushConferenceConfiguration(conversation, configuration, new OnConfigurationPushed() {
3394 @Override
3395 public void onPushSucceeded() {
3396 for (Jid invite : jids) {
3397 invite(conversation, invite);
3398 }
3399 for (String resource : account.getSelfContact().getPresences().toResourceArray()) {
3400 Jid other = account.getJid().withResource(resource);
3401 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending direct invite to " + other);
3402 directInvite(conversation, other);
3403 }
3404 saveConversationAsBookmark(conversation, name);
3405 if (callback != null) {
3406 callback.success(conversation);
3407 }
3408 }
3409
3410 @Override
3411 public void onPushFailed() {
3412 archiveConversation(conversation);
3413 if (callback != null) {
3414 callback.error(R.string.conference_creation_failed, conversation);
3415 }
3416 }
3417 });
3418 }
3419 });
3420 return true;
3421 } catch (IllegalArgumentException e) {
3422 if (callback != null) {
3423 callback.error(R.string.conference_creation_failed, null);
3424 }
3425 return false;
3426 }
3427 } else {
3428 if (callback != null) {
3429 callback.error(R.string.not_connected_try_again, null);
3430 }
3431 return false;
3432 }
3433 }
3434
3435 public void fetchConferenceConfiguration(final Conversation conversation) {
3436 fetchConferenceConfiguration(conversation, null);
3437 }
3438
3439 public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) {
3440 IqPacket request = mIqGenerator.queryDiscoInfo(conversation.getJid().asBareJid());
3441 sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
3442 @Override
3443 public void onIqPacketReceived(Account account, IqPacket packet) {
3444 if (packet.getType() == IqPacket.TYPE.RESULT) {
3445 final MucOptions mucOptions = conversation.getMucOptions();
3446 final Bookmark bookmark = conversation.getBookmark();
3447 final boolean sameBefore = StringUtils.equals(bookmark == null ? null : bookmark.getBookmarkName(), mucOptions.getName());
3448
3449 if (mucOptions.updateConfiguration(new ServiceDiscoveryResult(packet))) {
3450 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": muc configuration changed for " + conversation.getJid().asBareJid());
3451 updateConversation(conversation);
3452 }
3453
3454 if (bookmark != null && (sameBefore || bookmark.getBookmarkName() == null)) {
3455 if (bookmark.setBookmarkName(StringUtils.nullOnEmpty(mucOptions.getName()))) {
3456 createBookmark(account, bookmark);
3457 }
3458 }
3459
3460
3461 if (callback != null) {
3462 callback.onConferenceConfigurationFetched(conversation);
3463 }
3464
3465
3466 updateConversationUi();
3467 } else if (packet.getType() == IqPacket.TYPE.TIMEOUT) {
3468 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received timeout waiting for conference configuration fetch");
3469 } else {
3470 if (callback != null) {
3471 callback.onFetchFailed(conversation, packet.getErrorCondition());
3472 }
3473 }
3474 }
3475 });
3476 }
3477
3478 public void pushNodeConfiguration(Account account, final String node, final Bundle options, final OnConfigurationPushed callback) {
3479 pushNodeConfiguration(account, account.getJid().asBareJid(), node, options, callback);
3480 }
3481
3482 public void pushNodeConfiguration(Account account, final Jid jid, final String node, final Bundle options, final OnConfigurationPushed callback) {
3483 Log.d(Config.LOGTAG, "pushing node configuration");
3484 sendIqPacket(account, mIqGenerator.requestPubsubConfiguration(jid, node), new OnIqPacketReceived() {
3485 @Override
3486 public void onIqPacketReceived(Account account, IqPacket packet) {
3487 if (packet.getType() == IqPacket.TYPE.RESULT) {
3488 Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub#owner");
3489 Element configuration = pubsub == null ? null : pubsub.findChild("configure");
3490 Element x = configuration == null ? null : configuration.findChild("x", Namespace.DATA);
3491 if (x != null) {
3492 Data data = Data.parse(x);
3493 data.submit(options);
3494 sendIqPacket(account, mIqGenerator.publishPubsubConfiguration(jid, node, data), new OnIqPacketReceived() {
3495 @Override
3496 public void onIqPacketReceived(Account account, IqPacket packet) {
3497 if (packet.getType() == IqPacket.TYPE.RESULT && callback != null) {
3498 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully changed node configuration for node " + node);
3499 callback.onPushSucceeded();
3500 } else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) {
3501 callback.onPushFailed();
3502 }
3503 }
3504 });
3505 } else if (callback != null) {
3506 callback.onPushFailed();
3507 }
3508 } else if (packet.getType() == IqPacket.TYPE.ERROR && callback != null) {
3509 callback.onPushFailed();
3510 }
3511 }
3512 });
3513 }
3514
3515 public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConfigurationPushed callback) {
3516 if (options.getString("muc#roomconfig_whois", "moderators").equals("anyone")) {
3517 conversation.setAttribute("accept_non_anonymous", true);
3518 updateConversation(conversation);
3519 }
3520 if (options.containsKey("muc#roomconfig_moderatedroom")) {
3521 final boolean moderated = "1".equals(options.getString("muc#roomconfig_moderatedroom"));
3522 options.putString("members_by_default", moderated ? "0" : "1");
3523 }
3524 final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
3525 request.setTo(conversation.getJid().asBareJid());
3526 request.query("http://jabber.org/protocol/muc#owner");
3527 sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
3528 @Override
3529 public void onIqPacketReceived(Account account, IqPacket packet) {
3530 if (packet.getType() == IqPacket.TYPE.RESULT) {
3531 final Data data = Data.parse(packet.query().findChild("x", Namespace.DATA));
3532 data.submit(options);
3533 final IqPacket set = new IqPacket(IqPacket.TYPE.SET);
3534 set.setTo(conversation.getJid().asBareJid());
3535 set.query("http://jabber.org/protocol/muc#owner").addChild(data);
3536 sendIqPacket(account, set, new OnIqPacketReceived() {
3537 @Override
3538 public void onIqPacketReceived(Account account, IqPacket packet) {
3539 if (callback != null) {
3540 if (packet.getType() == IqPacket.TYPE.RESULT) {
3541 callback.onPushSucceeded();
3542 } else {
3543 callback.onPushFailed();
3544 }
3545 }
3546 }
3547 });
3548 } else {
3549 if (callback != null) {
3550 callback.onPushFailed();
3551 }
3552 }
3553 }
3554 });
3555 }
3556
3557 public void pushSubjectToConference(final Conversation conference, final String subject) {
3558 MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, StringUtils.nullOnEmpty(subject));
3559 this.sendMessagePacket(conference.getAccount(), packet);
3560 }
3561
3562 public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
3563 final Jid jid = user.asBareJid();
3564 final IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
3565 sendIqPacket(conference.getAccount(), request, (account, response) -> {
3566 if (response.getType() == IqPacket.TYPE.RESULT) {
3567 conference.getMucOptions().changeAffiliation(jid, affiliation);
3568 getAvatarService().clear(conference);
3569 if (callback != null) {
3570 callback.onAffiliationChangedSuccessful(jid);
3571 } else {
3572 Log.d(Config.LOGTAG, "changed affiliation of " + user + " to " + affiliation);
3573 }
3574 } else if (callback != null) {
3575 callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
3576 } else {
3577 Log.d(Config.LOGTAG, "unable to change affiliation");
3578 }
3579 });
3580 }
3581
3582 public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role) {
3583 IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
3584 sendIqPacket(conference.getAccount(), request, (account, packet) -> {
3585 if (packet.getType() != IqPacket.TYPE.RESULT) {
3586 Log.d(Config.LOGTAG, account.getJid().asBareJid() + " unable to change role of " + nick);
3587 }
3588 });
3589 }
3590
3591 public void moderateMessage(final Account account, final Message m, final String reason) {
3592 IqPacket request = this.mIqGenerator.moderateMessage(account, m, reason);
3593 sendIqPacket(account, request, (a, packet) -> {
3594 if (packet.getType() != IqPacket.TYPE.RESULT) {
3595 showErrorToastInUi(R.string.unable_to_moderate);
3596 Log.d(Config.LOGTAG, a.getJid().asBareJid() + " unable to moderate: " + packet);
3597 }
3598 });
3599 }
3600
3601 public void destroyRoom(final Conversation conversation, final OnRoomDestroy callback) {
3602 IqPacket request = new IqPacket(IqPacket.TYPE.SET);
3603 request.setTo(conversation.getJid().asBareJid());
3604 request.query("http://jabber.org/protocol/muc#owner").addChild("destroy");
3605 sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
3606 @Override
3607 public void onIqPacketReceived(Account account, IqPacket packet) {
3608 if (packet.getType() == IqPacket.TYPE.RESULT) {
3609 if (callback != null) {
3610 callback.onRoomDestroySucceeded();
3611 }
3612 } else if (packet.getType() == IqPacket.TYPE.ERROR) {
3613 if (callback != null) {
3614 callback.onRoomDestroyFailed();
3615 }
3616 }
3617 }
3618 });
3619 }
3620
3621 private void disconnect(Account account, boolean force) {
3622 if ((account.getStatus() == Account.State.ONLINE)
3623 || (account.getStatus() == Account.State.DISABLED)) {
3624 final XmppConnection connection = account.getXmppConnection();
3625 if (!force) {
3626 List<Conversation> conversations = getConversations();
3627 for (Conversation conversation : conversations) {
3628 if (conversation.getAccount() == account) {
3629 if (conversation.getMode() == Conversation.MODE_MULTI) {
3630 leaveMuc(conversation, true);
3631 }
3632 }
3633 }
3634 sendOfflinePresence(account);
3635 }
3636 connection.disconnect(force);
3637 }
3638 }
3639
3640 @Override
3641 public IBinder onBind(Intent intent) {
3642 return mBinder;
3643 }
3644
3645 public void updateMessage(Message message) {
3646 updateMessage(message, true);
3647 }
3648
3649 public void updateMessage(Message message, boolean includeBody) {
3650 databaseBackend.updateMessage(message, includeBody);
3651 updateConversationUi();
3652 }
3653
3654 public void createMessageAsync(final Message message) {
3655 mDatabaseWriterExecutor.execute(() -> databaseBackend.createMessage(message));
3656 }
3657
3658 public void updateMessage(Message message, String uuid) {
3659 if (!databaseBackend.updateMessage(message, uuid)) {
3660 Log.e(Config.LOGTAG, "error updated message in DB after edit");
3661 }
3662 updateConversationUi();
3663 }
3664
3665 protected void syncDirtyContacts(Account account) {
3666 for (Contact contact : account.getRoster().getContacts()) {
3667 if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
3668 pushContactToServer(contact);
3669 }
3670 if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
3671 deleteContactOnServer(contact);
3672 }
3673 }
3674 }
3675
3676 protected void unregisterPhoneAccounts(final Account account) {
3677 for (final Contact contact : account.getRoster().getContacts()) {
3678 if (!contact.showInRoster()) {
3679 contact.unregisterAsPhoneAccount(this);
3680 }
3681 }
3682 }
3683
3684 public void createContact(final Contact contact, final boolean autoGrant) {
3685 createContact(contact, autoGrant, null);
3686 }
3687
3688 public void createContact(final Contact contact, final boolean autoGrant, final String preAuth) {
3689 if (autoGrant) {
3690 contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
3691 contact.setOption(Contact.Options.ASKING);
3692 }
3693 pushContactToServer(contact, preAuth);
3694 }
3695
3696 public void pushContactToServer(final Contact contact) {
3697 pushContactToServer(contact, null);
3698 }
3699
3700 private void pushContactToServer(final Contact contact, final String preAuth) {
3701 contact.resetOption(Contact.Options.DIRTY_DELETE);
3702 contact.setOption(Contact.Options.DIRTY_PUSH);
3703 final Account account = contact.getAccount();
3704 if (account.getStatus() == Account.State.ONLINE) {
3705 final boolean ask = contact.getOption(Contact.Options.ASKING);
3706 final boolean sendUpdates = contact
3707 .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
3708 && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
3709 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
3710 iq.query(Namespace.ROSTER).addChild(contact.asElement());
3711 account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
3712 if (sendUpdates) {
3713 sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact));
3714 }
3715 if (ask) {
3716 sendPresencePacket(account, mPresenceGenerator.requestPresenceUpdatesFrom(contact, preAuth));
3717 }
3718 } else {
3719 syncRoster(contact.getAccount());
3720 }
3721 }
3722
3723 public void publishMucAvatar(final Conversation conversation, final Uri image, final OnAvatarPublication callback) {
3724 new Thread(() -> {
3725 final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
3726 final int size = Config.AVATAR_SIZE;
3727 final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
3728 if (avatar != null) {
3729 if (!getFileBackend().save(avatar)) {
3730 callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
3731 return;
3732 }
3733 avatar.owner = conversation.getJid().asBareJid();
3734 publishMucAvatar(conversation, avatar, callback);
3735 } else {
3736 callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
3737 }
3738 }).start();
3739 }
3740
3741 public void publishAvatar(final Account account, final Uri image, final OnAvatarPublication callback) {
3742 new Thread(() -> {
3743 final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
3744 final int size = Config.AVATAR_SIZE;
3745 final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
3746 if (avatar != null) {
3747 if (!getFileBackend().save(avatar)) {
3748 Log.d(Config.LOGTAG, "unable to save vcard");
3749 callback.onAvatarPublicationFailed(R.string.error_saving_avatar);
3750 return;
3751 }
3752 publishAvatar(account, avatar, callback);
3753 } else {
3754 callback.onAvatarPublicationFailed(R.string.error_publish_avatar_converting);
3755 }
3756 }).start();
3757
3758 }
3759
3760 private void publishMucAvatar(Conversation conversation, Avatar avatar, OnAvatarPublication callback) {
3761 final IqPacket retrieve = mIqGenerator.retrieveVcardAvatar(avatar);
3762 sendIqPacket(conversation.getAccount(), retrieve, (account, response) -> {
3763 boolean itemNotFound = response.getType() == IqPacket.TYPE.ERROR && response.hasChild("error") && response.findChild("error").hasChild("item-not-found");
3764 if (response.getType() == IqPacket.TYPE.RESULT || itemNotFound) {
3765 Element vcard = response.findChild("vCard", "vcard-temp");
3766 if (vcard == null) {
3767 vcard = new Element("vCard", "vcard-temp");
3768 }
3769 Element photo = vcard.findChild("PHOTO");
3770 if (photo == null) {
3771 photo = vcard.addChild("PHOTO");
3772 }
3773 photo.clearChildren();
3774 photo.addChild("TYPE").setContent(avatar.type);
3775 photo.addChild("BINVAL").setContent(avatar.image);
3776 IqPacket publication = new IqPacket(IqPacket.TYPE.SET);
3777 publication.setTo(conversation.getJid().asBareJid());
3778 publication.addChild(vcard);
3779 sendIqPacket(account, publication, (a1, publicationResponse) -> {
3780 if (publicationResponse.getType() == IqPacket.TYPE.RESULT) {
3781 callback.onAvatarPublicationSucceeded();
3782 } else {
3783 Log.d(Config.LOGTAG, "failed to publish vcard " + publicationResponse.getErrorCondition());
3784 callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
3785 }
3786 });
3787 } else {
3788 Log.d(Config.LOGTAG, "failed to request vcard " + response);
3789 callback.onAvatarPublicationFailed(R.string.error_publish_avatar_no_server_support);
3790 }
3791 });
3792 }
3793
3794 public void publishAvatar(Account account, final Avatar avatar, final OnAvatarPublication callback) {
3795 final Bundle options;
3796 if (account.getXmppConnection().getFeatures().pepPublishOptions()) {
3797 options = PublishOptions.openAccess();
3798 } else {
3799 options = null;
3800 }
3801 publishAvatar(account, avatar, options, true, callback);
3802 }
3803
3804 public void publishAvatar(Account account, final Avatar avatar, final Bundle options, final boolean retry, final OnAvatarPublication callback) {
3805 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": publishing avatar. options=" + options);
3806 IqPacket packet = this.mIqGenerator.publishAvatar(avatar, options);
3807 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
3808
3809 @Override
3810 public void onIqPacketReceived(Account account, IqPacket result) {
3811 if (result.getType() == IqPacket.TYPE.RESULT) {
3812 publishAvatarMetadata(account, avatar, options, true, callback);
3813 } else if (retry && PublishOptions.preconditionNotMet(result)) {
3814 pushNodeConfiguration(account, Namespace.AVATAR_DATA, options, new OnConfigurationPushed() {
3815 @Override
3816 public void onPushSucceeded() {
3817 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": changed node configuration for avatar node");
3818 publishAvatar(account, avatar, options, false, callback);
3819 }
3820
3821 @Override
3822 public void onPushFailed() {
3823 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to change node configuration for avatar node");
3824 publishAvatar(account, avatar, null, false, callback);
3825 }
3826 });
3827 } else {
3828 Element error = result.findChild("error");
3829 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": server rejected avatar " + (avatar.size / 1024) + "KiB " + (error != null ? error.toString() : ""));
3830 if (callback != null) {
3831 callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
3832 }
3833 }
3834 }
3835 });
3836 }
3837
3838 public void publishAvatarMetadata(Account account, final Avatar avatar, final Bundle options, final boolean retry, final OnAvatarPublication callback) {
3839 final IqPacket packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar, options);
3840 sendIqPacket(account, packet, new OnIqPacketReceived() {
3841 @Override
3842 public void onIqPacketReceived(Account account, IqPacket result) {
3843 if (result.getType() == IqPacket.TYPE.RESULT) {
3844 if (account.setAvatar(avatar.getFilename())) {
3845 getAvatarService().clear(account);
3846 databaseBackend.updateAccount(account);
3847 notifyAccountAvatarHasChanged(account);
3848 }
3849 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": published avatar " + (avatar.size / 1024) + "KiB");
3850 if (callback != null) {
3851 callback.onAvatarPublicationSucceeded();
3852 }
3853 } else if (retry && PublishOptions.preconditionNotMet(result)) {
3854 pushNodeConfiguration(account, Namespace.AVATAR_METADATA, options, new OnConfigurationPushed() {
3855 @Override
3856 public void onPushSucceeded() {
3857 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": changed node configuration for avatar meta data node");
3858 publishAvatarMetadata(account, avatar, options, false, callback);
3859 }
3860
3861 @Override
3862 public void onPushFailed() {
3863 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to change node configuration for avatar meta data node");
3864 publishAvatarMetadata(account, avatar, null, false, callback);
3865 }
3866 });
3867 } else {
3868 if (callback != null) {
3869 callback.onAvatarPublicationFailed(R.string.error_publish_avatar_server_reject);
3870 }
3871 }
3872 }
3873 });
3874 }
3875
3876 public void republishAvatarIfNeeded(Account account) {
3877 if (account.getAxolotlService().isPepBroken()) {
3878 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": skipping republication of avatar because pep is broken");
3879 return;
3880 }
3881 IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
3882 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
3883
3884 private Avatar parseAvatar(IqPacket packet) {
3885 Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
3886 if (pubsub != null) {
3887 Element items = pubsub.findChild("items");
3888 if (items != null) {
3889 return Avatar.parseMetadata(items);
3890 }
3891 }
3892 return null;
3893 }
3894
3895 private boolean errorIsItemNotFound(IqPacket packet) {
3896 Element error = packet.findChild("error");
3897 return packet.getType() == IqPacket.TYPE.ERROR
3898 && error != null
3899 && error.hasChild("item-not-found");
3900 }
3901
3902 @Override
3903 public void onIqPacketReceived(Account account, IqPacket packet) {
3904 if (packet.getType() == IqPacket.TYPE.RESULT || errorIsItemNotFound(packet)) {
3905 Avatar serverAvatar = parseAvatar(packet);
3906 if (serverAvatar == null && account.getAvatar() != null) {
3907 Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar());
3908 if (avatar != null) {
3909 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": avatar on server was null. republishing");
3910 publishAvatar(account, fileBackend.getStoredPepAvatar(account.getAvatar()), null);
3911 } else {
3912 Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": error rereading avatar");
3913 }
3914 }
3915 }
3916 }
3917 });
3918 }
3919
3920 public void fetchAvatar(Account account, Avatar avatar) {
3921 fetchAvatar(account, avatar, null);
3922 }
3923
3924 public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
3925 if (databaseBackend.isBlockedMedia(avatar.cid())) {
3926 if (callback != null) callback.error(0, null);
3927 return;
3928 }
3929
3930 final String KEY = generateFetchKey(account, avatar);
3931 synchronized (this.mInProgressAvatarFetches) {
3932 if (mInProgressAvatarFetches.add(KEY)) {
3933 switch (avatar.origin) {
3934 case PEP:
3935 this.mInProgressAvatarFetches.add(KEY);
3936 fetchAvatarPep(account, avatar, callback);
3937 break;
3938 case VCARD:
3939 this.mInProgressAvatarFetches.add(KEY);
3940 fetchAvatarVcard(account, avatar, callback);
3941 break;
3942 }
3943 } else if (avatar.origin == Avatar.Origin.PEP) {
3944 mOmittedPepAvatarFetches.add(KEY);
3945 } else {
3946 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": already fetching " + avatar.origin + " avatar for " + avatar.owner);
3947 }
3948 }
3949 }
3950
3951 private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
3952 IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar);
3953 sendIqPacket(account, packet, (a, result) -> {
3954 synchronized (mInProgressAvatarFetches) {
3955 mInProgressAvatarFetches.remove(generateFetchKey(a, avatar));
3956 }
3957 final String ERROR = a.getJid().asBareJid() + ": fetching avatar for " + avatar.owner + " failed ";
3958 if (result.getType() == IqPacket.TYPE.RESULT) {
3959 avatar.image = mIqParser.avatarData(result);
3960 if (avatar.image != null) {
3961 if (getFileBackend().save(avatar)) {
3962 if (a.getJid().asBareJid().equals(avatar.owner)) {
3963 if (a.setAvatar(avatar.getFilename())) {
3964 databaseBackend.updateAccount(a);
3965 }
3966 getAvatarService().clear(a);
3967 updateConversationUi();
3968 updateAccountUi();
3969 } else {
3970 final Contact contact = a.getRoster().getContact(avatar.owner);
3971 contact.setAvatar(avatar);
3972 syncRoster(account);
3973 getAvatarService().clear(contact);
3974 updateConversationUi();
3975 updateRosterUi();
3976 }
3977 if (callback != null) {
3978 callback.success(avatar);
3979 }
3980 Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": successfully fetched pep avatar for " + avatar.owner);
3981 return;
3982 }
3983 } else {
3984
3985 Log.d(Config.LOGTAG, ERROR + "(parsing error)");
3986 }
3987 } else {
3988 Element error = result.findChild("error");
3989 if (error == null) {
3990 Log.d(Config.LOGTAG, ERROR + "(server error)");
3991 } else {
3992 Log.d(Config.LOGTAG, ERROR + error.toString());
3993 }
3994 }
3995 if (callback != null) {
3996 callback.error(0, null);
3997 }
3998
3999 });
4000 }
4001
4002 private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
4003 IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
4004 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
4005 @Override
4006 public void onIqPacketReceived(Account account, IqPacket packet) {
4007 final boolean previouslyOmittedPepFetch;
4008 synchronized (mInProgressAvatarFetches) {
4009 final String KEY = generateFetchKey(account, avatar);
4010 mInProgressAvatarFetches.remove(KEY);
4011 previouslyOmittedPepFetch = mOmittedPepAvatarFetches.remove(KEY);
4012 }
4013 if (packet.getType() == IqPacket.TYPE.RESULT) {
4014 Element vCard = packet.findChild("vCard", "vcard-temp");
4015 Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
4016 String image = photo != null ? photo.findChildContent("BINVAL") : null;
4017 if (image != null) {
4018 avatar.image = image;
4019 if (getFileBackend().save(avatar)) {
4020 Log.d(Config.LOGTAG, account.getJid().asBareJid()
4021 + ": successfully fetched vCard avatar for " + avatar.owner + " omittedPep=" + previouslyOmittedPepFetch);
4022 if (avatar.owner.isBareJid()) {
4023 if (account.getJid().asBareJid().equals(avatar.owner) && account.getAvatar() == null) {
4024 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": had no avatar. replacing with vcard");
4025 account.setAvatar(avatar.getFilename());
4026 databaseBackend.updateAccount(account);
4027 getAvatarService().clear(account);
4028 updateAccountUi();
4029 } else {
4030 final Contact contact = account.getRoster().getContact(avatar.owner);
4031 contact.setAvatar(avatar, previouslyOmittedPepFetch);
4032 syncRoster(account);
4033 getAvatarService().clear(contact);
4034 updateRosterUi();
4035 }
4036 updateConversationUi();
4037 } else {
4038 Conversation conversation = find(account, avatar.owner.asBareJid());
4039 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
4040 MucOptions.User user = conversation.getMucOptions().findUserByFullJid(avatar.owner);
4041 if (user != null) {
4042 if (user.setAvatar(avatar)) {
4043 getAvatarService().clear(user);
4044 updateConversationUi();
4045 updateMucRosterUi();
4046 }
4047 if (user.getRealJid() != null) {
4048 Contact contact = account.getRoster().getContact(user.getRealJid());
4049 contact.setAvatar(avatar);
4050 syncRoster(account);
4051 getAvatarService().clear(contact);
4052 updateRosterUi();
4053 }
4054 }
4055 }
4056 }
4057 }
4058 }
4059 }
4060 }
4061 });
4062 }
4063
4064 public void checkForAvatar(Account account, final UiCallback<Avatar> callback) {
4065 IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
4066 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
4067
4068 @Override
4069 public void onIqPacketReceived(Account account, IqPacket packet) {
4070 if (packet.getType() == IqPacket.TYPE.RESULT) {
4071 Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
4072 if (pubsub != null) {
4073 Element items = pubsub.findChild("items");
4074 if (items != null) {
4075 Avatar avatar = Avatar.parseMetadata(items);
4076 if (avatar != null) {
4077 avatar.owner = account.getJid().asBareJid();
4078 if (fileBackend.isAvatarCached(avatar)) {
4079 if (account.setAvatar(avatar.getFilename())) {
4080 databaseBackend.updateAccount(account);
4081 }
4082 getAvatarService().clear(account);
4083 callback.success(avatar);
4084 } else {
4085 fetchAvatarPep(account, avatar, callback);
4086 }
4087 return;
4088 }
4089 }
4090 }
4091 }
4092 callback.error(0, null);
4093 }
4094 });
4095 }
4096
4097 public void notifyAccountAvatarHasChanged(final Account account) {
4098 final XmppConnection connection = account.getXmppConnection();
4099 if (connection != null && connection.getFeatures().bookmarksConversion()) {
4100 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": avatar changed. resending presence to online group chats");
4101 for (Conversation conversation : conversations) {
4102 if (conversation.getAccount() == account && conversation.getMode() == Conversational.MODE_MULTI) {
4103 final MucOptions mucOptions = conversation.getMucOptions();
4104 if (mucOptions.online()) {
4105 PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous());
4106 packet.setTo(mucOptions.getSelf().getFullJid());
4107 connection.sendPresencePacket(packet);
4108 }
4109 }
4110 }
4111 }
4112 }
4113
4114 public void deleteContactOnServer(Contact contact) {
4115 contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
4116 contact.resetOption(Contact.Options.DIRTY_PUSH);
4117 contact.setOption(Contact.Options.DIRTY_DELETE);
4118 Account account = contact.getAccount();
4119 if (account.getStatus() == Account.State.ONLINE) {
4120 IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
4121 Element item = iq.query(Namespace.ROSTER).addChild("item");
4122 item.setAttribute("jid", contact.getJid());
4123 item.setAttribute("subscription", "remove");
4124 account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
4125 }
4126 }
4127
4128 public void updateConversation(final Conversation conversation) {
4129 mDatabaseWriterExecutor.execute(() -> databaseBackend.updateConversation(conversation));
4130 }
4131
4132 private void reconnectAccount(final Account account, final boolean force, final boolean interactive) {
4133 synchronized (account) {
4134 XmppConnection connection = account.getXmppConnection();
4135 if (connection == null) {
4136 connection = createConnection(account);
4137 account.setXmppConnection(connection);
4138 }
4139 boolean hasInternet = hasInternetConnection();
4140 if (account.isEnabled() && hasInternet) {
4141 if (!force) {
4142 disconnect(account, false);
4143 }
4144 Thread thread = new Thread(connection);
4145 connection.setInteractive(interactive);
4146 connection.prepareNewConnection();
4147 connection.interrupt();
4148 thread.start();
4149 scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
4150 } else {
4151 disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
4152 account.getRoster().clearPresences();
4153 connection.resetEverything();
4154 final AxolotlService axolotlService = account.getAxolotlService();
4155 if (axolotlService != null) {
4156 axolotlService.resetBrokenness();
4157 }
4158 if (!hasInternet) {
4159 account.setStatus(Account.State.NO_INTERNET);
4160 }
4161 }
4162 }
4163 }
4164
4165 public void reconnectAccountInBackground(final Account account) {
4166 new Thread(() -> reconnectAccount(account, false, true)).start();
4167 }
4168
4169 public void invite(final Conversation conversation, final Jid contact) {
4170 Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": inviting " + contact + " to " + conversation.getJid().asBareJid());
4171 final MucOptions.User user = conversation.getMucOptions().findUserByRealJid(contact.asBareJid());
4172 if (user == null || user.getAffiliation() == MucOptions.Affiliation.OUTCAST) {
4173 changeAffiliationInConference(conversation, contact, MucOptions.Affiliation.NONE, null);
4174 }
4175 final MessagePacket packet = mMessageGenerator.invite(conversation, contact);
4176 sendMessagePacket(conversation.getAccount(), packet);
4177 }
4178
4179 public void directInvite(Conversation conversation, Jid jid) {
4180 MessagePacket packet = mMessageGenerator.directInvite(conversation, jid);
4181 sendMessagePacket(conversation.getAccount(), packet);
4182 }
4183
4184 public void resetSendingToWaiting(Account account) {
4185 for (Conversation conversation : getConversations()) {
4186 if (conversation.getAccount() == account) {
4187 conversation.findUnsentTextMessages(message -> markMessage(message, Message.STATUS_WAITING));
4188 }
4189 }
4190 }
4191
4192 public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) {
4193 return markMessage(account, recipient, uuid, status, null);
4194 }
4195
4196 public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status, String errorMessage) {
4197 if (uuid == null) {
4198 return null;
4199 }
4200 for (Conversation conversation : getConversations()) {
4201 if (conversation.getJid().asBareJid().equals(recipient) && conversation.getAccount() == account) {
4202 final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
4203 if (message != null) {
4204 markMessage(message, status, errorMessage);
4205 }
4206 return message;
4207 }
4208 }
4209 return null;
4210 }
4211
4212 public boolean markMessage(final Conversation conversation, final String uuid, final int status, final String serverMessageId) {
4213 return markMessage(conversation, uuid, status, serverMessageId, null);
4214 }
4215
4216 public boolean markMessage(final Conversation conversation, final String uuid, final int status, final String serverMessageId, final LocalizedContent body) {
4217 if (uuid == null) {
4218 return false;
4219 } else {
4220 final Message message = conversation.findSentMessageWithUuid(uuid);
4221 if (message != null) {
4222 if (message.getServerMsgId() == null) {
4223 message.setServerMsgId(serverMessageId);
4224 }
4225 if (message.getEncryption() == Message.ENCRYPTION_NONE
4226 && message.isTypeText()
4227 && isBodyModified(message, body)) {
4228 message.setBody(body.content);
4229 if (body.count > 1) {
4230 message.setBodyLanguage(body.language);
4231 }
4232 markMessage(message, status, null, true);
4233 } else {
4234 markMessage(message, status);
4235 }
4236 return true;
4237 } else {
4238 return false;
4239 }
4240 }
4241 }
4242
4243 private static boolean isBodyModified(final Message message, final LocalizedContent body) {
4244 if (body == null || body.content == null) {
4245 return false;
4246 }
4247 return !body.content.equals(message.getBody());
4248 }
4249
4250 public void markMessage(Message message, int status) {
4251 markMessage(message, status, null);
4252 }
4253
4254
4255 public void markMessage(final Message message, final int status, final String errorMessage) {
4256 markMessage(message, status, errorMessage, false);
4257 }
4258
4259 public void markMessage(final Message message, final int status, final String errorMessage, final boolean includeBody) {
4260 final int oldStatus = message.getStatus();
4261 if (status == Message.STATUS_SEND_FAILED && (oldStatus == Message.STATUS_SEND_RECEIVED || oldStatus == Message.STATUS_SEND_DISPLAYED)) {
4262 return;
4263 }
4264 if (status == Message.STATUS_SEND_RECEIVED && oldStatus == Message.STATUS_SEND_DISPLAYED) {
4265 return;
4266 }
4267 message.setErrorMessage(errorMessage);
4268 message.setStatus(status);
4269 databaseBackend.updateMessage(message, includeBody);
4270 updateConversationUi();
4271 if (oldStatus != status && status == Message.STATUS_SEND_FAILED) {
4272 mNotificationService.pushFailedDelivery(message);
4273 }
4274 }
4275
4276 public SharedPreferences getPreferences() {
4277 return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
4278 }
4279
4280 public long getAutomaticMessageDeletionDate() {
4281 final long timeout = getLongPreference(SettingsActivity.AUTOMATIC_MESSAGE_DELETION, R.integer.automatic_message_deletion);
4282 return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000));
4283 }
4284
4285 public long getLongPreference(String name, @IntegerRes int res) {
4286 long defaultValue = getResources().getInteger(res);
4287 try {
4288 return Long.parseLong(getPreferences().getString(name, String.valueOf(defaultValue)));
4289 } catch (NumberFormatException e) {
4290 return defaultValue;
4291 }
4292 }
4293
4294 public boolean getBooleanPreference(String name, @BoolRes int res) {
4295 return getPreferences().getBoolean(name, getResources().getBoolean(res));
4296 }
4297
4298 public boolean confirmMessages() {
4299 return getBooleanPreference("confirm_messages", R.bool.confirm_messages);
4300 }
4301
4302 public boolean allowMessageCorrection() {
4303 return getBooleanPreference("allow_message_correction", R.bool.allow_message_correction);
4304 }
4305
4306 public boolean sendChatStates() {
4307 return getBooleanPreference("chat_states", R.bool.chat_states);
4308 }
4309
4310 private boolean synchronizeWithBookmarks() {
4311 return getBooleanPreference("autojoin", R.bool.autojoin);
4312 }
4313
4314 public boolean useTorToConnect() {
4315 return getBooleanPreference("use_tor", R.bool.use_tor);
4316 }
4317
4318 public boolean showExtendedConnectionOptions() {
4319 return getBooleanPreference("show_connection_options", R.bool.show_connection_options);
4320 }
4321
4322 public boolean broadcastLastActivity() {
4323 return getBooleanPreference(SettingsActivity.BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
4324 }
4325
4326 public int unreadCount() {
4327 int count = 0;
4328 for (Conversation conversation : getConversations()) {
4329 count += conversation.unreadCount();
4330 }
4331 return count;
4332 }
4333
4334
4335 private <T> List<T> threadSafeList(Set<T> set) {
4336 synchronized (LISTENER_LOCK) {
4337 return set.size() == 0 ? Collections.emptyList() : new ArrayList<>(set);
4338 }
4339 }
4340
4341 public void showErrorToastInUi(int resId) {
4342 for (OnShowErrorToast listener : threadSafeList(this.mOnShowErrorToasts)) {
4343 listener.onShowErrorToast(resId);
4344 }
4345 }
4346
4347 public void updateConversationUi() {
4348 updateConversationUi(false);
4349 }
4350
4351 public void updateConversationUi(boolean newCaps) {
4352 for (OnConversationUpdate listener : threadSafeList(this.mOnConversationUpdates)) {
4353 listener.onConversationUpdate(newCaps);
4354 }
4355 }
4356
4357 public void notifyJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) {
4358 for (OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) {
4359 listener.onJingleRtpConnectionUpdate(account, with, sessionId, state);
4360 }
4361 }
4362
4363 public void notifyJingleRtpConnectionUpdate(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
4364 for (OnJingleRtpConnectionUpdate listener : threadSafeList(this.onJingleRtpConnectionUpdate)) {
4365 listener.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
4366 }
4367 }
4368
4369 public void updateAccountUi() {
4370 for (final OnAccountUpdate listener : threadSafeList(this.mOnAccountUpdates)) {
4371 listener.onAccountUpdate();
4372 }
4373 }
4374
4375 public void updateRosterUi() {
4376 for (OnRosterUpdate listener : threadSafeList(this.mOnRosterUpdates)) {
4377 listener.onRosterUpdate();
4378 }
4379 }
4380
4381 public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
4382 if (mOnCaptchaRequested.size() > 0) {
4383 DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
4384 Bitmap scaled = Bitmap.createScaledBitmap(captcha, (int) (captcha.getWidth() * metrics.scaledDensity),
4385 (int) (captcha.getHeight() * metrics.scaledDensity), false);
4386 for (OnCaptchaRequested listener : threadSafeList(this.mOnCaptchaRequested)) {
4387 listener.onCaptchaRequested(account, id, data, scaled);
4388 }
4389 return true;
4390 }
4391 return false;
4392 }
4393
4394 public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
4395 for (OnUpdateBlocklist listener : threadSafeList(this.mOnUpdateBlocklist)) {
4396 listener.OnUpdateBlocklist(status);
4397 }
4398 }
4399
4400 public void updateMucRosterUi() {
4401 for (OnMucRosterUpdate listener : threadSafeList(this.mOnMucRosterUpdate)) {
4402 listener.onMucRosterUpdate();
4403 }
4404 }
4405
4406 public void keyStatusUpdated(AxolotlService.FetchStatus report) {
4407 for (OnKeyStatusUpdated listener : threadSafeList(this.mOnKeyStatusUpdated)) {
4408 listener.onKeyStatusUpdated(report);
4409 }
4410 }
4411
4412 public Account findAccountByJid(final Jid jid) {
4413 for (final Account account : this.accounts) {
4414 if (account.getJid().asBareJid().equals(jid.asBareJid())) {
4415 return account;
4416 }
4417 }
4418 return null;
4419 }
4420
4421 public Account findAccountByUuid(final String uuid) {
4422 for (Account account : this.accounts) {
4423 if (account.getUuid().equals(uuid)) {
4424 return account;
4425 }
4426 }
4427 return null;
4428 }
4429
4430 public Conversation findConversationByUuid(String uuid) {
4431 for (Conversation conversation : getConversations()) {
4432 if (conversation.getUuid().equals(uuid)) {
4433 return conversation;
4434 }
4435 }
4436 return null;
4437 }
4438
4439 public Conversation findUniqueConversationByJid(XmppUri xmppUri) {
4440 List<Conversation> findings = new ArrayList<>();
4441 for (Conversation c : getConversations()) {
4442 if (c.getAccount().isEnabled() && c.getJid().asBareJid().equals(xmppUri.getJid().asBareJid()) && ((c.getMode() == Conversational.MODE_MULTI) == xmppUri.isAction(XmppUri.ACTION_JOIN))) {
4443 findings.add(c);
4444 }
4445 }
4446 return findings.size() == 1 ? findings.get(0) : null;
4447 }
4448
4449 public boolean markRead(final Conversation conversation, boolean dismiss) {
4450 return markRead(conversation, null, dismiss).size() > 0;
4451 }
4452
4453 public void markRead(final Conversation conversation) {
4454 markRead(conversation, null, true);
4455 }
4456
4457 public List<Message> markRead(final Conversation conversation, String upToUuid, boolean dismiss) {
4458 if (dismiss) {
4459 mNotificationService.clear(conversation);
4460 }
4461 final List<Message> readMessages = conversation.markRead(upToUuid);
4462 if (readMessages.size() > 0) {
4463 Runnable runnable = () -> {
4464 for (Message message : readMessages) {
4465 databaseBackend.updateMessage(message, false);
4466 }
4467 };
4468 mDatabaseWriterExecutor.execute(runnable);
4469 updateConversationUi();
4470 updateUnreadCountBadge();
4471 return readMessages;
4472 } else {
4473 return readMessages;
4474 }
4475 }
4476
4477 public synchronized void updateUnreadCountBadge() {
4478 int count = unreadCount();
4479 if (unreadCount != count) {
4480 Log.d(Config.LOGTAG, "update unread count to " + count);
4481 if (count > 0) {
4482 ShortcutBadger.applyCount(getApplicationContext(), count);
4483 } else {
4484 ShortcutBadger.removeCount(getApplicationContext());
4485 }
4486 unreadCount = count;
4487 }
4488 }
4489
4490 public void sendReadMarker(final Conversation conversation, String upToUuid) {
4491 final boolean isPrivateAndNonAnonymousMuc = conversation.getMode() == Conversation.MODE_MULTI && conversation.isPrivateAndNonAnonymous();
4492 final List<Message> readMessages = this.markRead(conversation, upToUuid, true);
4493 if (readMessages.size() > 0) {
4494 updateConversationUi();
4495 }
4496 final Message markable = Conversation.getLatestMarkableMessage(readMessages, isPrivateAndNonAnonymousMuc);
4497 if (confirmMessages()
4498 && markable != null
4499 && (markable.trusted() || isPrivateAndNonAnonymousMuc)
4500 && markable.getRemoteMsgId() != null) {
4501 Log.d(Config.LOGTAG, conversation.getAccount().getJid().asBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
4502 final Account account = conversation.getAccount();
4503 final MessagePacket packet = mMessageGenerator.confirm(markable);
4504 this.sendMessagePacket(account, packet);
4505 }
4506 }
4507
4508 public MemorizingTrustManager getMemorizingTrustManager() {
4509 return this.mMemorizingTrustManager;
4510 }
4511
4512 public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
4513 this.mMemorizingTrustManager = trustManager;
4514 }
4515
4516 public void updateMemorizingTrustmanager() {
4517 final MemorizingTrustManager tm;
4518 final boolean dontTrustSystemCAs = getBooleanPreference("dont_trust_system_cas", R.bool.dont_trust_system_cas);
4519 if (dontTrustSystemCAs) {
4520 tm = new MemorizingTrustManager(getApplicationContext(), null);
4521 } else {
4522 tm = new MemorizingTrustManager(getApplicationContext());
4523 }
4524 setMemorizingTrustManager(tm);
4525 }
4526
4527 public LruCache<String, Bitmap> getBitmapCache() {
4528 return this.mBitmapCache;
4529 }
4530
4531 public LruCache<String, Drawable> getDrawableCache() {
4532 return this.mDrawableCache;
4533 }
4534
4535 public Collection<String> getKnownHosts() {
4536 final Set<String> hosts = new HashSet<>();
4537 for (final Account account : getAccounts()) {
4538 hosts.add(account.getServer());
4539 for (final Contact contact : account.getRoster().getContacts()) {
4540 if (contact.showInRoster()) {
4541 final String server = contact.getServer();
4542 if (server != null) {
4543 hosts.add(server);
4544 }
4545 }
4546 }
4547 }
4548 if (Config.QUICKSY_DOMAIN != null) {
4549 hosts.remove(Config.QUICKSY_DOMAIN.toEscapedString()); //we only want to show this when we type a e164 number
4550 }
4551 if (Config.DOMAIN_LOCK != null) {
4552 hosts.add(Config.DOMAIN_LOCK);
4553 }
4554 if (Config.MAGIC_CREATE_DOMAIN != null) {
4555 hosts.add(Config.MAGIC_CREATE_DOMAIN);
4556 }
4557 return hosts;
4558 }
4559
4560 public Collection<String> getKnownConferenceHosts() {
4561 final Set<String> mucServers = new HashSet<>();
4562 for (final Account account : accounts) {
4563 if (account.getXmppConnection() != null) {
4564 mucServers.addAll(account.getXmppConnection().getMucServers());
4565 for (final Bookmark bookmark : account.getBookmarks()) {
4566 final Jid jid = bookmark.getJid();
4567 final String s = jid == null ? null : jid.getDomain().toEscapedString();
4568 if (s != null) {
4569 mucServers.add(s);
4570 }
4571 }
4572 }
4573 }
4574 return mucServers;
4575 }
4576
4577 public void sendMessagePacket(Account account, MessagePacket packet) {
4578 final XmppConnection connection = account.getXmppConnection();
4579 if (connection != null) {
4580 connection.sendMessagePacket(packet);
4581 }
4582 }
4583
4584 public void sendPresencePacket(Account account, PresencePacket packet) {
4585 XmppConnection connection = account.getXmppConnection();
4586 if (connection != null) {
4587 connection.sendPresencePacket(packet);
4588 }
4589 }
4590
4591 public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
4592 final XmppConnection connection = account.getXmppConnection();
4593 if (connection != null) {
4594 IqPacket request = mIqGenerator.generateCreateAccountWithCaptcha(account, id, data);
4595 connection.sendUnmodifiedIqPacket(request, connection.registrationResponseListener, true);
4596 }
4597 }
4598
4599 public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
4600 final XmppConnection connection = account.getXmppConnection();
4601 if (connection != null) {
4602 connection.sendIqPacket(packet, callback);
4603 } else if (callback != null) {
4604 callback.onIqPacketReceived(account, new IqPacket(IqPacket.TYPE.TIMEOUT));
4605 }
4606 }
4607
4608 public void sendPresence(final Account account) {
4609 sendPresence(account, checkListeners() && broadcastLastActivity());
4610 }
4611
4612 private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
4613 final Presence.Status status;
4614 if (manuallyChangePresence()) {
4615 status = account.getPresenceStatus();
4616 } else {
4617 status = getTargetPresence();
4618 }
4619 final PresencePacket packet = mPresenceGenerator.selfPresence(account, status);
4620 if (mLastActivity > 0 && includeIdleTimestamp) {
4621 long since = Math.min(mLastActivity, System.currentTimeMillis()); //don't send future dates
4622 packet.addChild("idle", Namespace.IDLE).setAttribute("since", AbstractGenerator.getTimestamp(since));
4623 }
4624 sendPresencePacket(account, packet);
4625 }
4626
4627 private void deactivateGracePeriod() {
4628 for (Account account : getAccounts()) {
4629 account.deactivateGracePeriod();
4630 }
4631 }
4632
4633 public void refreshAllPresences() {
4634 boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
4635 for (Account account : getAccounts()) {
4636 if (account.isEnabled()) {
4637 sendPresence(account, includeIdleTimestamp);
4638 }
4639 }
4640 }
4641
4642 private void refreshAllFcmTokens() {
4643 for (Account account : getAccounts()) {
4644 if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
4645 mPushManagementService.registerPushTokenOnServer(account);
4646 }
4647 }
4648 }
4649
4650
4651
4652 private void sendOfflinePresence(final Account account) {
4653 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": sending offline presence");
4654 sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
4655 }
4656
4657 public MessageGenerator getMessageGenerator() {
4658 return this.mMessageGenerator;
4659 }
4660
4661 public PresenceGenerator getPresenceGenerator() {
4662 return this.mPresenceGenerator;
4663 }
4664
4665 public IqGenerator getIqGenerator() {
4666 return this.mIqGenerator;
4667 }
4668
4669 public IqParser getIqParser() {
4670 return this.mIqParser;
4671 }
4672
4673 public JingleConnectionManager getJingleConnectionManager() {
4674 return this.mJingleConnectionManager;
4675 }
4676
4677 public MessageArchiveService getMessageArchiveService() {
4678 return this.mMessageArchiveService;
4679 }
4680
4681 public QuickConversationsService getQuickConversationsService() {
4682 return this.mQuickConversationsService;
4683 }
4684
4685 public List<Contact> findContacts(Jid jid, String accountJid) {
4686 ArrayList<Contact> contacts = new ArrayList<>();
4687 for (Account account : getAccounts()) {
4688 if ((account.isEnabled() || accountJid != null)
4689 && (accountJid == null || accountJid.equals(account.getJid().asBareJid().toString()))) {
4690 Contact contact = account.getRoster().getContactFromContactList(jid);
4691 if (contact != null) {
4692 contacts.add(contact);
4693 }
4694 }
4695 }
4696 return contacts;
4697 }
4698
4699 public Conversation findFirstMuc(Jid jid) {
4700 for (Conversation conversation : getConversations()) {
4701 if (conversation.getAccount().isEnabled() && conversation.getJid().asBareJid().equals(jid.asBareJid()) && conversation.getMode() == Conversation.MODE_MULTI) {
4702 return conversation;
4703 }
4704 }
4705 return null;
4706 }
4707
4708 public NotificationService getNotificationService() {
4709 return this.mNotificationService;
4710 }
4711
4712 public HttpConnectionManager getHttpConnectionManager() {
4713 return this.mHttpConnectionManager;
4714 }
4715
4716 public void resendFailedMessages(final Message message) {
4717 final Collection<Message> messages = new ArrayList<>();
4718 Message current = message;
4719 while (current.getStatus() == Message.STATUS_SEND_FAILED) {
4720 messages.add(current);
4721 if (current.mergeable(current.next())) {
4722 current = current.next();
4723 } else {
4724 break;
4725 }
4726 }
4727 for (final Message msg : messages) {
4728 msg.setTime(System.currentTimeMillis());
4729 markMessage(msg, Message.STATUS_WAITING);
4730 this.resendMessage(msg, false);
4731 }
4732 if (message.getConversation() instanceof Conversation) {
4733 ((Conversation) message.getConversation()).sort();
4734 }
4735 updateConversationUi();
4736 }
4737
4738 public void clearConversationHistory(final Conversation conversation) {
4739 final long clearDate;
4740 final String reference;
4741 if (conversation.countMessages() > 0) {
4742 Message latestMessage = conversation.getLatestMessage();
4743 clearDate = latestMessage.getTimeSent() + 1000;
4744 reference = latestMessage.getServerMsgId();
4745 } else {
4746 clearDate = System.currentTimeMillis();
4747 reference = null;
4748 }
4749 conversation.clearMessages();
4750 conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam
4751 conversation.setLastClearHistory(clearDate, reference);
4752 Runnable runnable = () -> {
4753 databaseBackend.deleteMessagesInConversation(conversation);
4754 databaseBackend.updateConversation(conversation);
4755 };
4756 mDatabaseWriterExecutor.execute(runnable);
4757 }
4758
4759 public boolean sendBlockRequest(final Blockable blockable, boolean reportSpam) {
4760 if (blockable != null && blockable.getBlockedJid() != null) {
4761 final Jid jid = blockable.getBlockedJid();
4762 this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam), (a, response) -> {
4763 if (response.getType() == IqPacket.TYPE.RESULT) {
4764 a.getBlocklist().add(jid);
4765 updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
4766 }
4767 });
4768 if (blockable.getBlockedJid().isFullJid()) {
4769 return false;
4770 } else if (removeBlockedConversations(blockable.getAccount(), jid)) {
4771 updateConversationUi();
4772 return true;
4773 } else {
4774 return false;
4775 }
4776 } else {
4777 return false;
4778 }
4779 }
4780
4781 public boolean removeBlockedConversations(final Account account, final Jid blockedJid) {
4782 boolean removed = false;
4783 synchronized (this.conversations) {
4784 boolean domainJid = blockedJid.getLocal() == null;
4785 for (Conversation conversation : this.conversations) {
4786 boolean jidMatches = (domainJid && blockedJid.getDomain().equals(conversation.getJid().getDomain()))
4787 || blockedJid.equals(conversation.getJid().asBareJid());
4788 if (conversation.getAccount() == account
4789 && conversation.getMode() == Conversation.MODE_SINGLE
4790 && jidMatches) {
4791 this.conversations.remove(conversation);
4792 markRead(conversation);
4793 conversation.setStatus(Conversation.STATUS_ARCHIVED);
4794 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": archiving conversation " + conversation.getJid().asBareJid() + " because jid was blocked");
4795 updateConversation(conversation);
4796 removed = true;
4797 }
4798 }
4799 }
4800 return removed;
4801 }
4802
4803 public void sendUnblockRequest(final Blockable blockable) {
4804 if (blockable != null && blockable.getJid() != null) {
4805 final Jid jid = blockable.getBlockedJid();
4806 this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() {
4807 @Override
4808 public void onIqPacketReceived(final Account account, final IqPacket packet) {
4809 if (packet.getType() == IqPacket.TYPE.RESULT) {
4810 account.getBlocklist().remove(jid);
4811 updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
4812 }
4813 }
4814 });
4815 }
4816 }
4817
4818 public void publishDisplayName(Account account) {
4819 String displayName = account.getDisplayName();
4820 final IqPacket request;
4821 if (TextUtils.isEmpty(displayName)) {
4822 request = mIqGenerator.deleteNode(Namespace.NICK);
4823 } else {
4824 request = mIqGenerator.publishNick(displayName);
4825 }
4826 mAvatarService.clear(account);
4827 sendIqPacket(account, request, (account1, packet) -> {
4828 if (packet.getType() == IqPacket.TYPE.ERROR) {
4829 Log.d(Config.LOGTAG, account1.getJid().asBareJid() + ": unable to modify nick name " + packet);
4830 }
4831 });
4832 }
4833
4834 public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String, String> key) {
4835 ServiceDiscoveryResult result = discoCache.get(key);
4836 if (result != null) {
4837 return result;
4838 } else {
4839 if (key.first == null || key.second == null) return null;
4840 result = databaseBackend.findDiscoveryResult(key.first, key.second);
4841 if (result != null) {
4842 discoCache.put(key, result);
4843 }
4844 return result;
4845 }
4846 }
4847
4848 public void fetchFromGateway(Account account, final Jid jid, final String input, final OnGatewayResult callback) {
4849 IqPacket request = new IqPacket(input == null ? IqPacket.TYPE.GET : IqPacket.TYPE.SET);
4850 request.setTo(jid);
4851 Element query = request.query("jabber:iq:gateway");
4852 if (input != null) {
4853 Element prompt = query.addChild("prompt");
4854 prompt.setContent(input);
4855 }
4856 sendIqPacket(account, request, (Account acct, IqPacket packet) -> {
4857 if (packet.getType() == IqPacket.TYPE.RESULT) {
4858 callback.onGatewayResult(packet.query().findChildContent(input == null ? "prompt" : "jid"), null);
4859 } else {
4860 Element error = packet.findChild("error");
4861 callback.onGatewayResult(null, error == null ? null : error.findChildContent("text"));
4862 }
4863 });
4864 }
4865
4866 public void fetchCaps(Account account, final Jid jid, final Presence presence) {
4867 fetchCaps(account, jid, presence, null);
4868 }
4869
4870 public void fetchCaps(Account account, final Jid jid, final Presence presence, Runnable cb) {
4871 final Pair<String, String> key = new Pair<>(presence.getHash(), presence.getVer());
4872 final ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key);
4873
4874 if (disco != null) {
4875 presence.setServiceDiscoveryResult(disco);
4876 final Contact contact = account.getRoster().getContact(jid);
4877 if (contact.refreshRtpCapability()) {
4878 syncRoster(account);
4879 }
4880 if (disco.hasIdentity("gateway", "pstn")) {
4881 contact.registerAsPhoneAccount(this);
4882 mQuickConversationsService.considerSyncBackground(false);
4883 }
4884 updateConversationUi(true);
4885 } else {
4886 final IqPacket request = new IqPacket(IqPacket.TYPE.GET);
4887 request.setTo(jid);
4888 final String node = presence.getNode();
4889 final String ver = presence.getVer();
4890 final Element query = request.query(Namespace.DISCO_INFO);
4891 if (node != null && ver != null) {
4892 query.setAttribute("node", node + "#" + ver);
4893 }
4894 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": making disco request for " + key.second + " to " + jid);
4895 sendIqPacket(account, request, (a, response) -> {
4896 if (response.getType() == IqPacket.TYPE.RESULT) {
4897 final ServiceDiscoveryResult discoveryResult = new ServiceDiscoveryResult(response);
4898 if (presence.getVer() == null || presence.getVer().equals(discoveryResult.getVer())) {
4899 databaseBackend.insertDiscoveryResult(discoveryResult);
4900 injectServiceDiscoveryResult(a.getRoster(), presence.getHash(), presence.getVer(), jid.getResource(), discoveryResult);
4901 if (discoveryResult.hasIdentity("gateway", "pstn")) {
4902 final Contact contact = account.getRoster().getContact(jid);
4903 contact.registerAsPhoneAccount(this);
4904 mQuickConversationsService.considerSyncBackground(false);
4905 }
4906 updateConversationUi(true);
4907 if (cb != null) cb.run();
4908 } else {
4909 Log.d(Config.LOGTAG, a.getJid().asBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + discoveryResult.getVer());
4910 }
4911 } else {
4912 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": unable to fetch caps from " + jid);
4913 }
4914 });
4915 }
4916 }
4917
4918 public void fetchCommands(Account account, final Jid jid, OnIqPacketReceived callback) {
4919 final IqPacket request = mIqGenerator.queryDiscoItems(jid, "http://jabber.org/protocol/commands");
4920 sendIqPacket(account, request, callback);
4921 }
4922
4923 private void injectServiceDiscoveryResult(Roster roster, String hash, String ver, String resource, ServiceDiscoveryResult disco) {
4924 boolean rosterNeedsSync = false;
4925 for (final Contact contact : roster.getContacts()) {
4926 boolean serviceDiscoverySet = false;
4927 Presence onePresence = contact.getPresences().get(resource == null ? "" : resource);
4928 if (onePresence != null) {
4929 onePresence.setServiceDiscoveryResult(disco);
4930 serviceDiscoverySet = true;
4931 }
4932 if (hash != null && ver != null) {
4933 for (final Presence presence : contact.getPresences().getPresences()) {
4934 if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
4935 presence.setServiceDiscoveryResult(disco);
4936 serviceDiscoverySet = true;
4937 }
4938 }
4939 }
4940 if (serviceDiscoverySet) {
4941 rosterNeedsSync |= contact.refreshRtpCapability();
4942 }
4943 }
4944 if (rosterNeedsSync) {
4945 syncRoster(roster.getAccount());
4946 }
4947 }
4948
4949 public void fetchMamPreferences(Account account, final OnMamPreferencesFetched callback) {
4950 final MessageArchiveService.Version version = MessageArchiveService.Version.get(account);
4951 IqPacket request = new IqPacket(IqPacket.TYPE.GET);
4952 request.addChild("prefs", version.namespace);
4953 sendIqPacket(account, request, (account1, packet) -> {
4954 Element prefs = packet.findChild("prefs", version.namespace);
4955 if (packet.getType() == IqPacket.TYPE.RESULT && prefs != null) {
4956 callback.onPreferencesFetched(prefs);
4957 } else {
4958 callback.onPreferencesFetchFailed();
4959 }
4960 });
4961 }
4962
4963 public PushManagementService getPushManagementService() {
4964 return mPushManagementService;
4965 }
4966
4967 public void changeStatus(Account account, PresenceTemplate template, String signature) {
4968 if (!template.getStatusMessage().isEmpty()) {
4969 databaseBackend.insertPresenceTemplate(template);
4970 }
4971 account.setPgpSignature(signature);
4972 account.setPresenceStatus(template.getStatus());
4973 account.setPresenceStatusMessage(template.getStatusMessage());
4974 databaseBackend.updateAccount(account);
4975 sendPresence(account);
4976 }
4977
4978 public List<PresenceTemplate> getPresenceTemplates(Account account) {
4979 List<PresenceTemplate> templates = databaseBackend.getPresenceTemplates();
4980 for (PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) {
4981 if (!templates.contains(template)) {
4982 templates.add(0, template);
4983 }
4984 }
4985 return templates;
4986 }
4987
4988 public void saveConversationAsBookmark(Conversation conversation, String name) {
4989 final Account account = conversation.getAccount();
4990 final Bookmark bookmark = new Bookmark(account, conversation.getJid().asBareJid());
4991 final String nick = conversation.getJid().getResource();
4992 if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
4993 bookmark.setNick(nick);
4994 }
4995 if (!TextUtils.isEmpty(name)) {
4996 bookmark.setBookmarkName(name);
4997 }
4998 bookmark.setAutojoin(getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin)));
4999 createBookmark(account, bookmark);
5000 bookmark.setConversation(conversation);
5001 }
5002
5003 public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
5004 boolean performedVerification = false;
5005 final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
5006 for (XmppUri.Fingerprint fp : fingerprints) {
5007 if (fp.type == XmppUri.FingerprintType.OMEMO) {
5008 String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
5009 FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
5010 if (fingerprintStatus != null) {
5011 if (!fingerprintStatus.isVerified()) {
5012 performedVerification = true;
5013 axolotlService.setFingerprintTrust(fingerprint, fingerprintStatus.toVerified());
5014 }
5015 } else {
5016 axolotlService.preVerifyFingerprint(contact, fingerprint);
5017 }
5018 }
5019 }
5020 return performedVerification;
5021 }
5022
5023 public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
5024 final AxolotlService axolotlService = account.getAxolotlService();
5025 boolean verifiedSomething = false;
5026 for (XmppUri.Fingerprint fp : fingerprints) {
5027 if (fp.type == XmppUri.FingerprintType.OMEMO) {
5028 String fingerprint = "05" + fp.fingerprint.replaceAll("\\s", "");
5029 Log.d(Config.LOGTAG, "trying to verify own fp=" + fingerprint);
5030 FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
5031 if (fingerprintStatus != null) {
5032 if (!fingerprintStatus.isVerified()) {
5033 axolotlService.setFingerprintTrust(fingerprint, fingerprintStatus.toVerified());
5034 verifiedSomething = true;
5035 }
5036 } else {
5037 axolotlService.preVerifyFingerprint(account, fingerprint);
5038 verifiedSomething = true;
5039 }
5040 }
5041 }
5042 return verifiedSomething;
5043 }
5044
5045 public boolean blindTrustBeforeVerification() {
5046 return getBooleanPreference(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, R.bool.btbv);
5047 }
5048
5049 public ShortcutService getShortcutService() {
5050 return mShortcutService;
5051 }
5052
5053 public void pushMamPreferences(Account account, Element prefs) {
5054 IqPacket set = new IqPacket(IqPacket.TYPE.SET);
5055 set.addChild(prefs);
5056 sendIqPacket(account, set, null);
5057 }
5058
5059 public void evictPreview(File f) {
5060 if (mBitmapCache.remove(f.getAbsolutePath()) != null) {
5061 Log.d(Config.LOGTAG, "deleted cached preview");
5062 }
5063 if (mDrawableCache.remove(f.getAbsolutePath()) != null) {
5064 Log.d(Config.LOGTAG, "deleted cached preview");
5065 }
5066 }
5067
5068 public void evictPreview(String uuid) {
5069 if (mBitmapCache.remove(uuid) != null) {
5070 Log.d(Config.LOGTAG, "deleted cached preview");
5071 }
5072 if (mDrawableCache.remove(uuid) != null) {
5073 Log.d(Config.LOGTAG, "deleted cached preview");
5074 }
5075 }
5076
5077 public interface OnMamPreferencesFetched {
5078 void onPreferencesFetched(Element prefs);
5079
5080 void onPreferencesFetchFailed();
5081 }
5082
5083 public interface OnAccountCreated {
5084 void onAccountCreated(Account account);
5085
5086 void informUser(int r);
5087 }
5088
5089 public interface OnMoreMessagesLoaded {
5090 void onMoreMessagesLoaded(int count, Conversation conversation);
5091
5092 void informUser(int r);
5093 }
5094
5095 public interface OnAccountPasswordChanged {
5096 void onPasswordChangeSucceeded();
5097
5098 void onPasswordChangeFailed();
5099 }
5100
5101 public interface OnRoomDestroy {
5102 void onRoomDestroySucceeded();
5103
5104 void onRoomDestroyFailed();
5105 }
5106
5107 public interface OnAffiliationChanged {
5108 void onAffiliationChangedSuccessful(Jid jid);
5109
5110 void onAffiliationChangeFailed(Jid jid, int resId);
5111 }
5112
5113 public interface OnConversationUpdate {
5114 default void onConversationUpdate() { onConversationUpdate(false); }
5115 default void onConversationUpdate(boolean newCaps) { onConversationUpdate(); }
5116 }
5117
5118 public interface OnJingleRtpConnectionUpdate {
5119 void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state);
5120
5121 void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices);
5122 }
5123
5124 public interface OnAccountUpdate {
5125 void onAccountUpdate();
5126 }
5127
5128 public interface OnCaptchaRequested {
5129 void onCaptchaRequested(Account account, String id, Data data, Bitmap captcha);
5130 }
5131
5132 public interface OnRosterUpdate {
5133 void onRosterUpdate();
5134 }
5135
5136 public interface OnMucRosterUpdate {
5137 void onMucRosterUpdate();
5138 }
5139
5140 public interface OnConferenceConfigurationFetched {
5141 void onConferenceConfigurationFetched(Conversation conversation);
5142
5143 void onFetchFailed(Conversation conversation, String errorCondition);
5144 }
5145
5146 public interface OnConferenceJoined {
5147 void onConferenceJoined(Conversation conversation);
5148 }
5149
5150 public interface OnConfigurationPushed {
5151 void onPushSucceeded();
5152
5153 void onPushFailed();
5154 }
5155
5156 public interface OnShowErrorToast {
5157 void onShowErrorToast(int resId);
5158 }
5159
5160 public class XmppConnectionBinder extends Binder {
5161 public XmppConnectionService getService() {
5162 return XmppConnectionService.this;
5163 }
5164 }
5165
5166 private class InternalEventReceiver extends BroadcastReceiver {
5167
5168 @Override
5169 public void onReceive(Context context, Intent intent) {
5170 onStartCommand(intent, 0, 0);
5171 }
5172 }
5173
5174 public static class OngoingCall {
5175 public final AbstractJingleConnection.Id id;
5176 public final Set<Media> media;
5177 public final boolean reconnecting;
5178
5179 public OngoingCall(AbstractJingleConnection.Id id, Set<Media> media, final boolean reconnecting) {
5180 this.id = id;
5181 this.media = media;
5182 this.reconnecting = reconnecting;
5183 }
5184
5185 @Override
5186 public boolean equals(Object o) {
5187 if (this == o) return true;
5188 if (o == null || getClass() != o.getClass()) return false;
5189 OngoingCall that = (OngoingCall) o;
5190 return reconnecting == that.reconnecting && Objects.equal(id, that.id) && Objects.equal(media, that.media);
5191 }
5192
5193 @Override
5194 public int hashCode() {
5195 return Objects.hashCode(id, media, reconnecting);
5196 }
5197 }
5198
5199 public static class BlockedMediaException extends Exception { }
5200}