1package eu.siacs.conversations.services;
2
3import android.annotation.SuppressLint;
4import android.annotation.TargetApi;
5import android.app.AlarmManager;
6import android.app.PendingIntent;
7import android.app.Service;
8import android.content.Context;
9import android.content.Intent;
10import android.content.IntentFilter;
11import android.content.SharedPreferences;
12import android.database.ContentObserver;
13import android.graphics.Bitmap;
14import android.media.AudioManager;
15import android.net.ConnectivityManager;
16import android.net.NetworkInfo;
17import android.net.Uri;
18import android.os.Binder;
19import android.os.Build;
20import android.os.Bundle;
21import android.os.Environment;
22import android.os.IBinder;
23import android.os.ParcelFileDescriptor;
24import android.os.PowerManager;
25import android.os.PowerManager.WakeLock;
26import android.os.SystemClock;
27import android.preference.PreferenceManager;
28import android.provider.ContactsContract;
29import android.security.KeyChain;
30import android.support.annotation.BoolRes;
31import android.support.annotation.IntegerRes;
32import android.support.v4.app.RemoteInput;
33import android.util.DisplayMetrics;
34import android.util.Log;
35import android.util.LruCache;
36import android.util.Pair;
37
38import net.java.otr4j.OtrException;
39import net.java.otr4j.session.Session;
40import net.java.otr4j.session.SessionID;
41import net.java.otr4j.session.SessionImpl;
42import net.java.otr4j.session.SessionStatus;
43import net.ypresto.androidtranscoder.MediaTranscoder;
44import net.ypresto.androidtranscoder.format.MediaFormatStrategy;
45import net.ypresto.androidtranscoder.format.MediaFormatStrategyPresets;
46
47import org.openintents.openpgp.IOpenPgpService2;
48import org.openintents.openpgp.util.OpenPgpApi;
49import org.openintents.openpgp.util.OpenPgpServiceConnection;
50
51import java.io.FileDescriptor;
52import java.io.FileNotFoundException;
53import java.math.BigInteger;
54import java.net.URL;
55import java.security.SecureRandom;
56import java.security.cert.CertificateException;
57import java.security.cert.X509Certificate;
58import java.util.ArrayList;
59import java.util.Arrays;
60import java.util.Collection;
61import java.util.Collections;
62import java.util.HashMap;
63import java.util.HashSet;
64import java.util.Hashtable;
65import java.util.Iterator;
66import java.util.List;
67import java.util.ListIterator;
68import java.util.Locale;
69import java.util.Map;
70import java.util.concurrent.CopyOnWriteArrayList;
71import java.util.concurrent.atomic.AtomicBoolean;
72import java.util.concurrent.atomic.AtomicLong;
73
74import de.duenndns.ssl.MemorizingTrustManager;
75import eu.siacs.conversations.Config;
76import eu.siacs.conversations.R;
77import eu.siacs.conversations.crypto.PgpDecryptionService;
78import eu.siacs.conversations.crypto.PgpEngine;
79import eu.siacs.conversations.crypto.axolotl.AxolotlService;
80import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
81import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
82import eu.siacs.conversations.entities.Account;
83import eu.siacs.conversations.entities.Blockable;
84import eu.siacs.conversations.entities.Bookmark;
85import eu.siacs.conversations.entities.Contact;
86import eu.siacs.conversations.entities.Conversation;
87import eu.siacs.conversations.entities.DownloadableFile;
88import eu.siacs.conversations.entities.Message;
89import eu.siacs.conversations.entities.MucOptions;
90import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
91import eu.siacs.conversations.entities.Presence;
92import eu.siacs.conversations.entities.PresenceTemplate;
93import eu.siacs.conversations.entities.Roster;
94import eu.siacs.conversations.entities.ServiceDiscoveryResult;
95import eu.siacs.conversations.entities.Transferable;
96import eu.siacs.conversations.entities.TransferablePlaceholder;
97import eu.siacs.conversations.generator.AbstractGenerator;
98import eu.siacs.conversations.generator.IqGenerator;
99import eu.siacs.conversations.generator.MessageGenerator;
100import eu.siacs.conversations.generator.PresenceGenerator;
101import eu.siacs.conversations.http.HttpConnectionManager;
102import eu.siacs.conversations.http.AesGcmURLStreamHandlerFactory;
103import eu.siacs.conversations.parser.AbstractParser;
104import eu.siacs.conversations.parser.IqParser;
105import eu.siacs.conversations.parser.MessageParser;
106import eu.siacs.conversations.parser.PresenceParser;
107import eu.siacs.conversations.persistance.DatabaseBackend;
108import eu.siacs.conversations.persistance.FileBackend;
109import eu.siacs.conversations.ui.SettingsActivity;
110import eu.siacs.conversations.ui.UiCallback;
111import eu.siacs.conversations.ui.UiInformableCallback;
112import eu.siacs.conversations.utils.ConversationsFileObserver;
113import eu.siacs.conversations.utils.CryptoHelper;
114import eu.siacs.conversations.utils.ExceptionHelper;
115import eu.siacs.conversations.utils.MimeUtils;
116import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
117import eu.siacs.conversations.utils.PRNGFixes;
118import eu.siacs.conversations.utils.PhoneHelper;
119import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
120import eu.siacs.conversations.utils.Resolver;
121import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
122import eu.siacs.conversations.xml.Namespace;
123import eu.siacs.conversations.utils.XmppUri;
124import eu.siacs.conversations.xml.Element;
125import eu.siacs.conversations.xmpp.OnBindListener;
126import eu.siacs.conversations.xmpp.OnContactStatusChanged;
127import eu.siacs.conversations.xmpp.OnIqPacketReceived;
128import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
129import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
130import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
131import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
132import eu.siacs.conversations.xmpp.OnStatusChanged;
133import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
134import eu.siacs.conversations.xmpp.Patches;
135import eu.siacs.conversations.xmpp.XmppConnection;
136import eu.siacs.conversations.xmpp.chatstate.ChatState;
137import eu.siacs.conversations.xmpp.forms.Data;
138import eu.siacs.conversations.xmpp.jid.InvalidJidException;
139import eu.siacs.conversations.xmpp.jid.Jid;
140import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
141import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
142import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
143import eu.siacs.conversations.xmpp.mam.MamReference;
144import eu.siacs.conversations.xmpp.pep.Avatar;
145import eu.siacs.conversations.xmpp.stanzas.IqPacket;
146import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
147import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
148import me.leolin.shortcutbadger.ShortcutBadger;
149
150public class XmppConnectionService extends Service {
151
152 static {
153 URL.setURLStreamHandlerFactory(new AesGcmURLStreamHandlerFactory());
154 }
155
156 public static final String ACTION_REPLY_TO_CONVERSATION = "reply_to_conversations";
157 public static final String ACTION_MARK_AS_READ = "mark_as_read";
158 public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification";
159 public static final String ACTION_DISMISS_ERROR_NOTIFICATIONS = "dismiss_error";
160 public static final String ACTION_TRY_AGAIN = "try_again";
161 public static final String ACTION_IDLE_PING = "idle_ping";
162 private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
163 public static final String ACTION_GCM_TOKEN_REFRESH = "gcm_token_refresh";
164 public static final String ACTION_GCM_MESSAGE_RECEIVED = "gcm_message_received";
165 private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor();
166 private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor();
167 private ReplacingSerialSingleThreadExecutor mContactMergerExecutor = new ReplacingSerialSingleThreadExecutor(true);
168 private final IBinder mBinder = new XmppConnectionBinder();
169 private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
170 private final IqGenerator mIqGenerator = new IqGenerator(this);
171 private final List<String> mInProgressAvatarFetches = new ArrayList<>();
172 private final HashSet<Jid> mLowPingTimeoutMode = new HashSet<>();
173
174 private long mLastActivity = 0;
175
176 public DatabaseBackend databaseBackend;
177 private ContentObserver contactObserver = new ContentObserver(null) {
178 @Override
179 public void onChange(boolean selfChange) {
180 super.onChange(selfChange);
181 Intent intent = new Intent(getApplicationContext(),
182 XmppConnectionService.class);
183 intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
184 startService(intent);
185 }
186 };
187 private FileBackend fileBackend = new FileBackend(this);
188 private MemorizingTrustManager mMemorizingTrustManager;
189 private NotificationService mNotificationService = new NotificationService(this);
190 private ShortcutService mShortcutService = new ShortcutService(this);
191 private AtomicBoolean mInitialAddressbookSyncCompleted = new AtomicBoolean(false);
192 private AtomicBoolean mForceForegroundService = new AtomicBoolean(false);
193 private OnMessagePacketReceived mMessageParser = new MessageParser(this);
194 private OnPresencePacketReceived mPresenceParser = new PresenceParser(this);
195 private IqParser mIqParser = new IqParser(this);
196 private OnIqPacketReceived mDefaultIqHandler = new OnIqPacketReceived() {
197 @Override
198 public void onIqPacketReceived(Account account, IqPacket packet) {
199 if (packet.getType() != IqPacket.TYPE.RESULT) {
200 Element error = packet.findChild("error");
201 String text = error != null ? error.findChildContent("text") : null;
202 if (text != null) {
203 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": received iq error - " + text);
204 }
205 }
206 }
207 };
208 private MessageGenerator mMessageGenerator = new MessageGenerator(this);
209 private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
210 private List<Account> accounts;
211 private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
212 this);
213 public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
214
215 @Override
216 public void onContactStatusChanged(Contact contact, boolean online) {
217 Conversation conversation = find(getConversations(), contact);
218 if (conversation != null) {
219 if (online) {
220 conversation.endOtrIfNeeded();
221 if (contact.getPresences().size() == 1) {
222 sendUnsentMessages(conversation);
223 }
224 } else {
225 //check if the resource we are haveing a conversation with is still online
226 if (conversation.hasValidOtrSession()) {
227 String otrResource = conversation.getOtrSession().getSessionID().getUserID();
228 if (!(Arrays.asList(contact.getPresences().toResourceArray()).contains(otrResource))) {
229 conversation.endOtrIfNeeded();
230 }
231 }
232 }
233 }
234 }
235 };
236 private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
237 this);
238 private AvatarService mAvatarService = new AvatarService(this);
239 private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
240 private PushManagementService mPushManagementService = new PushManagementService(this);
241 private OnConversationUpdate mOnConversationUpdate = null;
242
243
244 private final ConversationsFileObserver fileObserver = new ConversationsFileObserver(
245 Environment.getExternalStorageDirectory().getAbsolutePath()
246 ) {
247 @Override
248 public void onEvent(int event, String path) {
249 markFileDeleted(path);
250 }
251 };
252 private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
253
254 @Override
255 public void onJinglePacketReceived(Account account, JinglePacket packet) {
256 mJingleConnectionManager.deliverPacket(account, packet);
257 }
258 };
259 private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
260
261 @Override
262 public void onMessageAcknowledged(Account account, String uuid) {
263 for (final Conversation conversation : getConversations()) {
264 if (conversation.getAccount() == account) {
265 Message message = conversation.findUnsentMessageWithUuid(uuid);
266 if (message != null) {
267 markMessage(message, Message.STATUS_SEND);
268 }
269 }
270 }
271 }
272 };
273 private int convChangedListenerCount = 0;
274 private OnShowErrorToast mOnShowErrorToast = null;
275 private int showErrorToastListenerCount = 0;
276 private int unreadCount = -1;
277 private OnAccountUpdate mOnAccountUpdate = null;
278 private OnCaptchaRequested mOnCaptchaRequested = null;
279 private int accountChangedListenerCount = 0;
280 private int captchaRequestedListenerCount = 0;
281 private OnRosterUpdate mOnRosterUpdate = null;
282 private OnUpdateBlocklist mOnUpdateBlocklist = null;
283 private int updateBlocklistListenerCount = 0;
284 private int rosterChangedListenerCount = 0;
285 private OnMucRosterUpdate mOnMucRosterUpdate = null;
286 private int mucRosterChangedListenerCount = 0;
287 private OnKeyStatusUpdated mOnKeyStatusUpdated = null;
288 private int keyStatusUpdatedListenerCount = 0;
289 private AtomicLong mLastExpiryRun = new AtomicLong(0);
290 private SecureRandom mRandom;
291 private LruCache<Pair<String,String>,ServiceDiscoveryResult> discoCache = new LruCache<>(20);
292 private final OnBindListener mOnBindListener = new OnBindListener() {
293
294 @Override
295 public void onBind(final Account account) {
296 synchronized (mInProgressAvatarFetches) {
297 for (Iterator<String> iterator = mInProgressAvatarFetches.iterator(); iterator.hasNext(); ) {
298 final String KEY = iterator.next();
299 if (KEY.startsWith(account.getJid().toBareJid() + "_")) {
300 iterator.remove();
301 }
302 }
303 }
304 if (account.setOption(Account.OPTION_LOGGED_IN_SUCCESSFULLY,true)) {
305 databaseBackend.updateAccount(account);
306 }
307 account.getRoster().clearPresences();
308 mJingleConnectionManager.cancelInTransmission();
309 fetchRosterFromServer(account);
310 fetchBookmarks(account);
311 sendPresence(account);
312 if (mPushManagementService.available(account)) {
313 mPushManagementService.registerPushTokenOnServer(account);
314 }
315 connectMultiModeConversations(account);
316 syncDirtyContacts(account);
317 }
318 };
319 private OnStatusChanged statusListener = new OnStatusChanged() {
320
321 @Override
322 public void onStatusChanged(final Account account) {
323 XmppConnection connection = account.getXmppConnection();
324 if (mOnAccountUpdate != null) {
325 mOnAccountUpdate.onAccountUpdate();
326 }
327 if (account.getStatus() == Account.State.ONLINE) {
328 synchronized (mLowPingTimeoutMode) {
329 if (mLowPingTimeoutMode.remove(account.getJid().toBareJid())) {
330 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": leaving low ping timeout mode");
331 }
332 }
333 if (account.setShowErrorNotification(true)) {
334 databaseBackend.updateAccount(account);
335 }
336 mMessageArchiveService.executePendingQueries(account);
337 if (connection != null && connection.getFeatures().csi()) {
338 if (checkListeners()) {
339 Log.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//inactive");
340 connection.sendInactive();
341 } else {
342 Log.d(Config.LOGTAG, account.getJid().toBareJid() + " sending csi//active");
343 connection.sendActive();
344 }
345 }
346 List<Conversation> conversations = getConversations();
347 for (Conversation conversation : conversations) {
348 if (conversation.getAccount() == account
349 && !account.pendingConferenceJoins.contains(conversation)) {
350 if (!conversation.startOtrIfNeeded()) {
351 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": couldn't start OTR with "+conversation.getContact().getJid()+" when needed");
352 }
353 sendUnsentMessages(conversation);
354 }
355 }
356 for (Conversation conversation : account.pendingConferenceLeaves) {
357 leaveMuc(conversation);
358 }
359 account.pendingConferenceLeaves.clear();
360 for (Conversation conversation : account.pendingConferenceJoins) {
361 joinMuc(conversation);
362 }
363 account.pendingConferenceJoins.clear();
364 scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
365 } else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) {
366 resetSendingToWaiting(account);
367 if (!account.isOptionSet(Account.OPTION_DISABLED) && isInLowPingTimeoutMode(account)) {
368 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": went into offline state during low ping mode. reconnecting now");
369 reconnectAccount(account, true, false);
370 } else {
371 int timeToReconnect = mRandom.nextInt(10) + 2;
372 scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
373 }
374 } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
375 databaseBackend.updateAccount(account);
376 reconnectAccount(account, true, false);
377 } else if (account.getStatus() != Account.State.CONNECTING && account.getStatus() != Account.State.NO_INTERNET) {
378 resetSendingToWaiting(account);
379 if (connection != null && account.getStatus().isAttemptReconnect()) {
380 final int next = connection.getTimeToNextAttempt();
381 final boolean lowPingTimeoutMode = isInLowPingTimeoutMode(account);
382 if (next <= 0) {
383 Log.d(Config.LOGTAG, account.getJid().toBareJid()+": error connecting account. reconnecting now. lowPingTimeout="+Boolean.toString(lowPingTimeoutMode));
384 reconnectAccount(account, true, false);
385 } else {
386 final int attempt = connection.getAttempt() + 1;
387 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": error connecting account. try again in " + next + "s for the " + attempt+ " time. lowPingTimeout="+Boolean.toString(lowPingTimeoutMode));
388 scheduleWakeUpCall(next, account.getUuid().hashCode());
389 }
390 }
391 }
392 getNotificationService().updateErrorNotification();
393 }
394 };
395
396 private boolean isInLowPingTimeoutMode(Account account) {
397 synchronized (mLowPingTimeoutMode) {
398 return mLowPingTimeoutMode.contains(account.getJid().toBareJid());
399 }
400 }
401
402 public void startForcingForegroundNotification() {
403 mForceForegroundService.set(true);
404 toggleForegroundService();
405 }
406
407 public void stopForcingForegroundNotification() {
408 mForceForegroundService.set(false);
409 toggleForegroundService();
410 }
411
412 private OpenPgpServiceConnection pgpServiceConnection;
413 private PgpEngine mPgpEngine = null;
414 private WakeLock wakeLock;
415 private PowerManager pm;
416 private LruCache<String, Bitmap> mBitmapCache;
417 private EventReceiver mEventReceiver = new EventReceiver();
418
419 private boolean mRestoredFromDatabase = false;
420
421 private static String generateFetchKey(Account account, final Avatar avatar) {
422 return account.getJid().toBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum;
423 }
424
425 public boolean areMessagesInitialized() {
426 return this.mRestoredFromDatabase;
427 }
428
429 public PgpEngine getPgpEngine() {
430 if (!Config.supportOpenPgp()) {
431 return null;
432 } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
433 if (this.mPgpEngine == null) {
434 this.mPgpEngine = new PgpEngine(new OpenPgpApi(
435 getApplicationContext(),
436 pgpServiceConnection.getService()), this);
437 }
438 return mPgpEngine;
439 } else {
440 return null;
441 }
442
443 }
444
445 public OpenPgpApi getOpenPgpApi() {
446 if (!Config.supportOpenPgp()) {
447 return null;
448 } else if (pgpServiceConnection != null && pgpServiceConnection.isBound()) {
449 return new OpenPgpApi(this, pgpServiceConnection.getService());
450 } else {
451 return null;
452 }
453 }
454
455 public FileBackend getFileBackend() {
456 return this.fileBackend;
457 }
458
459 public AvatarService getAvatarService() {
460 return this.mAvatarService;
461 }
462
463 public void attachLocationToConversation(final Conversation conversation,
464 final Uri uri,
465 final UiCallback<Message> callback) {
466 int encryption = conversation.getNextEncryption();
467 if (encryption == Message.ENCRYPTION_PGP) {
468 encryption = Message.ENCRYPTION_DECRYPTED;
469 }
470 Message message = new Message(conversation, uri.toString(), encryption);
471 if (conversation.getNextCounterpart() != null) {
472 message.setCounterpart(conversation.getNextCounterpart());
473 }
474 if (encryption == Message.ENCRYPTION_DECRYPTED) {
475 getPgpEngine().encrypt(message, callback);
476 } else {
477 callback.success(message);
478 }
479 }
480
481 public void attachFileToConversation(final Conversation conversation,
482 final Uri uri,
483 final UiCallback<Message> callback) {
484 if (FileBackend.weOwnFile(this, uri)) {
485 Log.d(Config.LOGTAG,"trying to attach file that belonged to us");
486 callback.error(R.string.security_error_invalid_file_access, null);
487 return;
488 }
489 final Message message;
490 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
491 message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
492 } else {
493 message = new Message(conversation, "", conversation.getNextEncryption());
494 }
495 message.setCounterpart(conversation.getNextCounterpart());
496 message.setType(Message.TYPE_FILE);
497 AttachFileToConversationRunnable runnable = new AttachFileToConversationRunnable(this,uri,message,callback);
498 mFileAddingExecutor.execute(runnable);
499 }
500
501 public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
502 if (FileBackend.weOwnFile(this, uri)) {
503 Log.d(Config.LOGTAG,"trying to attach file that belonged to us");
504 callback.error(R.string.security_error_invalid_file_access, null);
505 return;
506 }
507
508 final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri);
509 final String compressPictures = getCompressPicturesPreference();
510
511 if ("never".equals(compressPictures)
512 || ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))
513 || (mimeType != null && mimeType.endsWith("/gif"))) {
514 Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+ ": not compressing picture. sending as file");
515 attachFileToConversation(conversation, uri, callback);
516 return;
517 }
518 final Message message;
519 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
520 message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
521 } else {
522 message = new Message(conversation, "", conversation.getNextEncryption());
523 }
524 message.setCounterpart(conversation.getNextCounterpart());
525 message.setType(Message.TYPE_IMAGE);
526 mFileAddingExecutor.execute(new Runnable() {
527
528 @Override
529 public void run() {
530 try {
531 getFileBackend().copyImageToPrivateStorage(message, uri);
532 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
533 final PgpEngine pgpEngine = getPgpEngine();
534 if (pgpEngine != null) {
535 pgpEngine.encrypt(message, callback);
536 } else if (callback != null){
537 callback.error(R.string.unable_to_connect_to_keychain, null);
538 }
539 } else {
540 callback.success(message);
541 }
542 } catch (final FileBackend.FileCopyException e) {
543 callback.error(e.getResId(), message);
544 }
545 }
546 });
547 }
548
549 public Conversation find(Bookmark bookmark) {
550 return find(bookmark.getAccount(), bookmark.getJid());
551 }
552
553 public Conversation find(final Account account, final Jid jid) {
554 return find(getConversations(), account, jid);
555 }
556
557 @Override
558 public int onStartCommand(Intent intent, int flags, int startId) {
559 final String action = intent == null ? null : intent.getAction();
560 String pushedAccountHash = null;
561 boolean interactive = false;
562 if (action != null) {
563 final String uuid = intent.getStringExtra("uuid");
564 final Conversation c = findConversationByUuid(uuid);
565 switch (action) {
566 case ConnectivityManager.CONNECTIVITY_ACTION:
567 if (hasInternetConnection() && Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
568 resetAllAttemptCounts(true, false);
569 }
570 break;
571 case ACTION_MERGE_PHONE_CONTACTS:
572 if (mRestoredFromDatabase) {
573 loadPhoneContacts();
574 }
575 return START_STICKY;
576 case Intent.ACTION_SHUTDOWN:
577 logoutAndSave(true);
578 return START_NOT_STICKY;
579 case ACTION_CLEAR_NOTIFICATION:
580 if (c != null) {
581 mNotificationService.clear(c);
582 } else {
583 mNotificationService.clear();
584 }
585 break;
586 case ACTION_DISMISS_ERROR_NOTIFICATIONS:
587 dismissErrorNotifications();
588 break;
589 case ACTION_TRY_AGAIN:
590 resetAllAttemptCounts(false, true);
591 interactive = true;
592 break;
593 case ACTION_REPLY_TO_CONVERSATION:
594 Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
595 if (remoteInput != null && c != null) {
596 final CharSequence body = remoteInput.getCharSequence("text_reply");
597 if (body != null && body.length() > 0) {
598 directReply(c, body.toString(),intent.getBooleanExtra("dismiss_notification",false));
599 }
600 }
601 break;
602 case ACTION_MARK_AS_READ:
603 if (c != null) {
604 sendReadMarker(c);
605 } else {
606 Log.d(Config.LOGTAG,"received mark read intent for unknown conversation ("+uuid+")");
607 }
608 break;
609 case AudioManager.RINGER_MODE_CHANGED_ACTION:
610 if (dndOnSilentMode()) {
611 refreshAllPresences();
612 }
613 break;
614 case Intent.ACTION_SCREEN_ON:
615 deactivateGracePeriod();
616 case Intent.ACTION_SCREEN_OFF:
617 if (awayWhenScreenOff()) {
618 refreshAllPresences();
619 }
620 break;
621 case ACTION_GCM_TOKEN_REFRESH:
622 refreshAllGcmTokens();
623 break;
624 case ACTION_IDLE_PING:
625 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
626 scheduleNextIdlePing();
627 }
628 break;
629 case ACTION_GCM_MESSAGE_RECEIVED:
630 Log.d(Config.LOGTAG,"gcm push message arrived in service. extras="+intent.getExtras());
631 pushedAccountHash = intent.getStringExtra("account");
632 break;
633 }
634 }
635 synchronized (this) {
636 this.wakeLock.acquire();
637 boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action);
638 HashSet<Account> pingCandidates = new HashSet<>();
639 for (Account account : accounts) {
640 pingNow |= processAccountState(account,
641 interactive,
642 "ui".equals(action),
643 CryptoHelper.getAccountFingerprint(account).equals(pushedAccountHash),
644 pingCandidates);
645 }
646 if (pingNow) {
647 for (Account account : pingCandidates) {
648 final boolean lowTimeout = isInLowPingTimeoutMode(account);
649 account.getXmppConnection().sendPing();
650 Log.d(Config.LOGTAG, account.getJid().toBareJid() + " send ping (action=" + action + ",lowTimeout=" + Boolean.toString(lowTimeout) + ")");
651 scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode());
652 }
653 }
654 if (wakeLock.isHeld()) {
655 try {
656 wakeLock.release();
657 } catch (final RuntimeException ignored) {
658 }
659 }
660 }
661 if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) {
662 expireOldMessages();
663 }
664 return START_STICKY;
665 }
666
667 private boolean processAccountState(Account account, boolean interactive, boolean isUiAction, boolean isAccountPushed, HashSet<Account> pingCandidates) {
668 boolean pingNow = false;
669 if (account.getStatus().isAttemptReconnect()) {
670 if (!hasInternetConnection()) {
671 account.setStatus(Account.State.NO_INTERNET);
672 if (statusListener != null) {
673 statusListener.onStatusChanged(account);
674 }
675 } else {
676 if (account.getStatus() == Account.State.NO_INTERNET) {
677 account.setStatus(Account.State.OFFLINE);
678 if (statusListener != null) {
679 statusListener.onStatusChanged(account);
680 }
681 }
682 if (account.getStatus() == Account.State.ONLINE) {
683 synchronized (mLowPingTimeoutMode) {
684 long lastReceived = account.getXmppConnection().getLastPacketReceived();
685 long lastSent = account.getXmppConnection().getLastPingSent();
686 long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
687 long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime();
688 int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().toBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000;
689 long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
690 if (lastSent > lastReceived) {
691 if (pingTimeoutIn < 0) {
692 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ping timeout");
693 this.reconnectAccount(account, true, interactive);
694 } else {
695 int secs = (int) (pingTimeoutIn / 1000);
696 this.scheduleWakeUpCall(secs, account.getUuid().hashCode());
697 }
698 } else {
699 pingCandidates.add(account);
700 if (isAccountPushed) {
701 pingNow = true;
702 if (mLowPingTimeoutMode.add(account.getJid().toBareJid())) {
703 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": entering low ping timeout mode");
704 }
705 } else if (msToNextPing <= 0) {
706 pingNow = true;
707 } else {
708 this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
709 if (mLowPingTimeoutMode.remove(account.getJid().toBareJid())) {
710 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": leaving low ping timeout mode");
711 }
712 }
713 }
714 }
715 } else if (account.getStatus() == Account.State.OFFLINE) {
716 reconnectAccount(account, true, interactive);
717 } else if (account.getStatus() == Account.State.CONNECTING) {
718 long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
719 long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
720 long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
721 long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
722 if (timeout < 0) {
723 Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast="+secondsSinceLastConnect+")");
724 account.getXmppConnection().resetAttemptCount(false);
725 reconnectAccount(account, true, interactive);
726 } else if (discoTimeout < 0) {
727 account.getXmppConnection().sendDiscoTimeout();
728 scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
729 } else {
730 scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
731 }
732 } else {
733 if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
734 reconnectAccount(account, true, interactive);
735 }
736 }
737 }
738 }
739 return pingNow;
740 }
741
742 public boolean isDataSaverDisabled() {
743 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
744 ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);
745 return !connectivityManager.isActiveNetworkMetered()
746 || connectivityManager.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
747 } else {
748 return true;
749 }
750 }
751
752 private void directReply(Conversation conversation, String body, final boolean dismissAfterReply) {
753 Message message = new Message(conversation,body,conversation.getNextEncryption());
754 message.markUnread();
755 if (message.getEncryption() == Message.ENCRYPTION_PGP) {
756 getPgpEngine().encrypt(message, new UiCallback<Message>() {
757 @Override
758 public void success(Message message) {
759 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
760 sendMessage(message);
761 if (dismissAfterReply) {
762 markRead(message.getConversation(),true);
763 } else {
764 mNotificationService.pushFromDirectReply(message);
765 }
766 }
767
768 @Override
769 public void error(int errorCode, Message object) {
770
771 }
772
773 @Override
774 public void userInputRequried(PendingIntent pi, Message object) {
775
776 }
777 });
778 } else {
779 sendMessage(message);
780 if (dismissAfterReply) {
781 markRead(conversation,true);
782 } else {
783 mNotificationService.pushFromDirectReply(message);
784 }
785 }
786 }
787
788 private boolean dndOnSilentMode() {
789 return getBooleanPreference(SettingsActivity.DND_ON_SILENT_MODE, R.bool.dnd_on_silent_mode);
790 }
791
792 private boolean manuallyChangePresence() {
793 return getBooleanPreference(SettingsActivity.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
794 }
795
796 private boolean treatVibrateAsSilent() {
797 return getBooleanPreference(SettingsActivity.TREAT_VIBRATE_AS_SILENT, R.bool.treat_vibrate_as_silent);
798 }
799
800 private boolean awayWhenScreenOff() {
801 return getBooleanPreference(SettingsActivity.AWAY_WHEN_SCREEN_IS_OFF,R.bool.away_when_screen_off);
802 }
803
804 private String getCompressPicturesPreference() {
805 return getPreferences().getString("picture_compression", getResources().getString(R.string.picture_compression));
806 }
807
808 private Presence.Status getTargetPresence() {
809 if (dndOnSilentMode() && isPhoneSilenced()) {
810 return Presence.Status.DND;
811 } else if (awayWhenScreenOff() && !isInteractive()) {
812 return Presence.Status.AWAY;
813 } else {
814 return Presence.Status.ONLINE;
815 }
816 }
817
818 @SuppressLint("NewApi")
819 @SuppressWarnings("deprecation")
820 public boolean isInteractive() {
821 final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
822
823 final boolean isScreenOn;
824 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
825 isScreenOn = pm.isScreenOn();
826 } else {
827 isScreenOn = pm.isInteractive();
828 }
829 return isScreenOn;
830 }
831
832 private boolean isPhoneSilenced() {
833 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
834 try {
835 if (treatVibrateAsSilent()) {
836 return audioManager.getRingerMode() != AudioManager.RINGER_MODE_NORMAL;
837 } else {
838 return audioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT;
839 }
840 } catch (Throwable throwable) {
841 Log.d(Config.LOGTAG,"platform bug in isPhoneSilenced ("+ throwable.getMessage()+")");
842 return false;
843 }
844 }
845
846 private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) {
847 Log.d(Config.LOGTAG, "resetting all attempt counts");
848 for (Account account : accounts) {
849 if (account.hasErrorStatus() || reallyAll) {
850 final XmppConnection connection = account.getXmppConnection();
851 if (connection != null) {
852 connection.resetAttemptCount(retryImmediately);
853 }
854 }
855 if (account.setShowErrorNotification(true)) {
856 databaseBackend.updateAccount(account);
857 }
858 }
859 mNotificationService.updateErrorNotification();
860 }
861
862 private void dismissErrorNotifications() {
863 for (final Account account : this.accounts) {
864 if (account.hasErrorStatus()) {
865 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": dismissing error notification");
866 if (account.setShowErrorNotification(false)) {
867 databaseBackend.updateAccount(account);
868 }
869 }
870 }
871 }
872
873 private void expireOldMessages() {
874 expireOldMessages(false);
875 }
876
877 public void expireOldMessages(final boolean resetHasMessagesLeftOnServer) {
878 mLastExpiryRun.set(SystemClock.elapsedRealtime());
879 mDatabaseExecutor.execute(new Runnable() {
880 @Override
881 public void run() {
882 long timestamp = getAutomaticMessageDeletionDate();
883 if (timestamp > 0) {
884 databaseBackend.expireOldMessages(timestamp);
885 synchronized (XmppConnectionService.this.conversations) {
886 for (Conversation conversation : XmppConnectionService.this.conversations) {
887 conversation.expireOldMessages(timestamp);
888 if (resetHasMessagesLeftOnServer) {
889 conversation.messagesLoaded.set(true);
890 conversation.setHasMessagesLeftOnServer(true);
891 }
892 }
893 }
894 updateConversationUi();
895 }
896 }
897 });
898 }
899
900 public boolean hasInternetConnection() {
901 ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
902 .getSystemService(Context.CONNECTIVITY_SERVICE);
903 NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
904 return activeNetwork != null && activeNetwork.isConnected();
905 }
906
907 @SuppressLint("TrulyRandom")
908 @Override
909 public void onCreate() {
910 ExceptionHelper.init(getApplicationContext());
911 PRNGFixes.apply();
912 Resolver.registerXmppConnectionService(this);
913 this.mRandom = new SecureRandom();
914 updateMemorizingTrustmanager();
915 final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
916 final int cacheSize = maxMemory / 8;
917 this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
918 @Override
919 protected int sizeOf(final String key, final Bitmap bitmap) {
920 return bitmap.getByteCount() / 1024;
921 }
922 };
923
924 Log.d(Config.LOGTAG,"initializing database...");
925 this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
926 Log.d(Config.LOGTAG,"restoring accounts...");
927 this.accounts = databaseBackend.getAccounts();
928
929 if (this.accounts.size() == 0 && Arrays.asList("Sony","Sony Ericsson").contains(Build.MANUFACTURER)) {
930 getPreferences().edit().putBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE,true).commit();
931 Log.d(Config.LOGTAG,Build.MANUFACTURER+" is on blacklist. enabling foreground service");
932 }
933
934 restoreFromDatabase();
935
936 getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
937 new Thread(new Runnable() {
938 @Override
939 public void run() {
940 fileObserver.startWatching();
941 }
942 }).start();
943 if (Config.supportOpenPgp()) {
944 this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain", new OpenPgpServiceConnection.OnBound() {
945 @Override
946 public void onBound(IOpenPgpService2 service) {
947 for (Account account : accounts) {
948 final PgpDecryptionService pgp = account.getPgpDecryptionService();
949 if(pgp != null) {
950 pgp.continueDecryption(true);
951 }
952 }
953 }
954
955 @Override
956 public void onError(Exception e) {
957 }
958 });
959 this.pgpServiceConnection.bindToService();
960 }
961
962 this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
963 this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "XmppConnectionService");
964
965 toggleForegroundService();
966 updateUnreadCountBadge();
967 toggleScreenEventReceiver();
968 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
969 scheduleNextIdlePing();
970 }
971 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
972 registerReceiver(this.mEventReceiver,new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
973 }
974 }
975
976 @Override
977 public void onTrimMemory(int level) {
978 super.onTrimMemory(level);
979 if (level >= TRIM_MEMORY_COMPLETE) {
980 Log.d(Config.LOGTAG, "clear cache due to low memory");
981 getBitmapCache().evictAll();
982 }
983 }
984
985 @Override
986 public void onDestroy() {
987 try {
988 unregisterReceiver(this.mEventReceiver);
989 } catch (IllegalArgumentException e) {
990 //ignored
991 }
992 fileObserver.stopWatching();
993 super.onDestroy();
994 }
995
996 public void toggleScreenEventReceiver() {
997 if (awayWhenScreenOff() && !manuallyChangePresence()) {
998 final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
999 filter.addAction(Intent.ACTION_SCREEN_OFF);
1000 registerReceiver(this.mEventReceiver, filter);
1001 } else {
1002 try {
1003 unregisterReceiver(this.mEventReceiver);
1004 } catch (IllegalArgumentException e) {
1005 //ignored
1006 }
1007 }
1008 }
1009
1010 public void toggleForegroundService() {
1011 if (mForceForegroundService.get() || (keepForegroundService() && hasEnabledAccounts())) {
1012 startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
1013 Log.d(Config.LOGTAG,"started foreground service");
1014 } else {
1015 stopForeground(true);
1016 Log.d(Config.LOGTAG,"stopped foreground service");
1017 }
1018 }
1019
1020 public boolean keepForegroundService() {
1021 return getBooleanPreference(SettingsActivity.KEEP_FOREGROUND_SERVICE,R.bool.enable_foreground_service);
1022 }
1023
1024 @Override
1025 public void onTaskRemoved(final Intent rootIntent) {
1026 super.onTaskRemoved(rootIntent);
1027 if (keepForegroundService() || mForceForegroundService.get()) {
1028 Log.d(Config.LOGTAG,"ignoring onTaskRemoved because foreground service is activated");
1029 } else {
1030 this.logoutAndSave(false);
1031 }
1032 }
1033
1034 private void logoutAndSave(boolean stop) {
1035 int activeAccounts = 0;
1036 for (final Account account : accounts) {
1037 if (account.getStatus() != Account.State.DISABLED) {
1038 activeAccounts++;
1039 }
1040 databaseBackend.writeRoster(account.getRoster());
1041 if (account.getXmppConnection() != null) {
1042 new Thread(new Runnable() {
1043 @Override
1044 public void run() {
1045 disconnect(account, false);
1046 }
1047 }).start();
1048 }
1049 }
1050 if (stop || activeAccounts == 0) {
1051 Log.d(Config.LOGTAG, "good bye");
1052 stopSelf();
1053 }
1054 }
1055
1056 public void scheduleWakeUpCall(int seconds, int requestCode) {
1057 final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000;
1058 AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
1059 Intent intent = new Intent(this, EventReceiver.class);
1060 intent.setAction("ping");
1061 PendingIntent alarmIntent = PendingIntent.getBroadcast(this, requestCode, intent, 0);
1062 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, alarmIntent);
1063 }
1064
1065 @TargetApi(Build.VERSION_CODES.M)
1066 private void scheduleNextIdlePing() {
1067 Log.d(Config.LOGTAG,"schedule next idle ping");
1068 AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
1069 Intent intent = new Intent(this, EventReceiver.class);
1070 intent.setAction(ACTION_IDLE_PING);
1071 alarmManager.setAndAllowWhileIdle(AlarmManager.ELAPSED_REALTIME_WAKEUP,
1072 SystemClock.elapsedRealtime()+(Config.IDLE_PING_INTERVAL * 1000),
1073 PendingIntent.getBroadcast(this,0,intent,0)
1074 );
1075 }
1076
1077 public XmppConnection createConnection(final Account account) {
1078 final SharedPreferences sharedPref = getPreferences();
1079 String resource;
1080 try {
1081 resource = sharedPref.getString("resource", getString(R.string.default_resource)).toLowerCase(Locale.ENGLISH);
1082 if (resource.trim().isEmpty()) {
1083 throw new Exception();
1084 }
1085 } catch (Exception e) {
1086 resource = "conversations";
1087 }
1088 account.setResource(resource);
1089 final XmppConnection connection = new XmppConnection(account, this);
1090 connection.setOnMessagePacketReceivedListener(this.mMessageParser);
1091 connection.setOnStatusChangedListener(this.statusListener);
1092 connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
1093 connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
1094 connection.setOnJinglePacketReceivedListener(this.jingleListener);
1095 connection.setOnBindListener(this.mOnBindListener);
1096 connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
1097 connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
1098 connection.addOnAdvancedStreamFeaturesAvailableListener(this.mAvatarService);
1099 AxolotlService axolotlService = account.getAxolotlService();
1100 if (axolotlService != null) {
1101 connection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
1102 }
1103 return connection;
1104 }
1105
1106 public void sendChatState(Conversation conversation) {
1107 if (sendChatStates()) {
1108 MessagePacket packet = mMessageGenerator.generateChatState(conversation);
1109 sendMessagePacket(conversation.getAccount(), packet);
1110 }
1111 }
1112
1113 private void sendFileMessage(final Message message, final boolean delay) {
1114 Log.d(Config.LOGTAG, "send file message");
1115 final Account account = message.getConversation().getAccount();
1116 if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize())
1117 || message.getConversation().getMode() == Conversation.MODE_MULTI) {
1118 mHttpConnectionManager.createNewUploadConnection(message, delay);
1119 } else {
1120 mJingleConnectionManager.createNewConnection(message);
1121 }
1122 }
1123
1124 public void sendMessage(final Message message) {
1125 sendMessage(message, false, false);
1126 }
1127
1128 private void sendMessage(final Message message, final boolean resend, final boolean delay) {
1129 final Account account = message.getConversation().getAccount();
1130 if (account.setShowErrorNotification(true)) {
1131 databaseBackend.updateAccount(account);
1132 mNotificationService.updateErrorNotification();
1133 }
1134 final Conversation conversation = message.getConversation();
1135 account.deactivateGracePeriod();
1136 MessagePacket packet = null;
1137 final boolean addToConversation = (conversation.getMode() != Conversation.MODE_MULTI
1138 || !Patches.BAD_MUC_REFLECTION.contains(account.getServerIdentity()))
1139 && !message.edited();
1140 boolean saveInDb = addToConversation;
1141 message.setStatus(Message.STATUS_WAITING);
1142
1143 if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) {
1144 message.getConversation().endOtrIfNeeded();
1145 message.getConversation().findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR,
1146 new Conversation.OnMessageFound() {
1147 @Override
1148 public void onMessageFound(Message message) {
1149 markMessage(message, Message.STATUS_SEND_FAILED);
1150 }
1151 });
1152 }
1153
1154 if (account.isOnlineAndConnected()) {
1155 switch (message.getEncryption()) {
1156 case Message.ENCRYPTION_NONE:
1157 if (message.needsUploading()) {
1158 if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize())
1159 || conversation.getMode() == Conversation.MODE_MULTI
1160 || message.fixCounterpart()) {
1161 this.sendFileMessage(message, delay);
1162 } else {
1163 break;
1164 }
1165 } else {
1166 packet = mMessageGenerator.generateChat(message);
1167 }
1168 break;
1169 case Message.ENCRYPTION_PGP:
1170 case Message.ENCRYPTION_DECRYPTED:
1171 if (message.needsUploading()) {
1172 if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize())
1173 || conversation.getMode() == Conversation.MODE_MULTI
1174 || message.fixCounterpart()) {
1175 this.sendFileMessage(message, delay);
1176 } else {
1177 break;
1178 }
1179 } else {
1180 packet = mMessageGenerator.generatePgpChat(message);
1181 }
1182 break;
1183 case Message.ENCRYPTION_OTR:
1184 SessionImpl otrSession = conversation.getOtrSession();
1185 if (otrSession != null && otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
1186 try {
1187 message.setCounterpart(Jid.fromSessionID(otrSession.getSessionID()));
1188 } catch (InvalidJidException e) {
1189 break;
1190 }
1191 if (message.needsUploading()) {
1192 mJingleConnectionManager.createNewConnection(message);
1193 } else {
1194 packet = mMessageGenerator.generateOtrChat(message);
1195 }
1196 } else if (otrSession == null) {
1197 if (message.fixCounterpart()) {
1198 conversation.startOtrSession(message.getCounterpart().getResourcepart(), true);
1199 } else {
1200 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not fix counterpart for OTR message to contact "+message.getCounterpart());
1201 break;
1202 }
1203 } else {
1204 Log.d(Config.LOGTAG,account.getJid().toBareJid()+" OTR session with "+message.getContact()+" is in wrong state: "+otrSession.getSessionStatus().toString());
1205 }
1206 break;
1207 case Message.ENCRYPTION_AXOLOTL:
1208 message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
1209 if (message.needsUploading()) {
1210 if (account.httpUploadAvailable(fileBackend.getFile(message,false).getSize())
1211 || conversation.getMode() == Conversation.MODE_MULTI
1212 || message.fixCounterpart()) {
1213 this.sendFileMessage(message, delay);
1214 } else {
1215 break;
1216 }
1217 } else {
1218 XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message);
1219 if (axolotlMessage == null) {
1220 account.getAxolotlService().preparePayloadMessage(message, delay);
1221 } else {
1222 packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
1223 }
1224 }
1225 break;
1226
1227 }
1228 if (packet != null) {
1229 if (account.getXmppConnection().getFeatures().sm()
1230 || (conversation.getMode() == Conversation.MODE_MULTI && message.getCounterpart().isBareJid())) {
1231 message.setStatus(Message.STATUS_UNSEND);
1232 } else {
1233 message.setStatus(Message.STATUS_SEND);
1234 }
1235 }
1236 } else {
1237 switch (message.getEncryption()) {
1238 case Message.ENCRYPTION_DECRYPTED:
1239 if (!message.needsUploading()) {
1240 String pgpBody = message.getEncryptedBody();
1241 String decryptedBody = message.getBody();
1242 message.setBody(pgpBody);
1243 message.setEncryption(Message.ENCRYPTION_PGP);
1244 if (message.edited()) {
1245 message.setBody(decryptedBody);
1246 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
1247 databaseBackend.updateMessage(message, message.getEditedId());
1248 updateConversationUi();
1249 return;
1250 } else {
1251 databaseBackend.createMessage(message);
1252 saveInDb = false;
1253 message.setBody(decryptedBody);
1254 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
1255 }
1256 }
1257 break;
1258 case Message.ENCRYPTION_OTR:
1259 if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) {
1260 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": create otr session without starting for "+message.getContact().getJid());
1261 conversation.startOtrSession(message.getCounterpart().getResourcepart(), false);
1262 }
1263 break;
1264 case Message.ENCRYPTION_AXOLOTL:
1265 message.setFingerprint(account.getAxolotlService().getOwnFingerprint());
1266 break;
1267 }
1268 }
1269
1270 if (resend) {
1271 if (packet != null && addToConversation) {
1272 if (account.getXmppConnection().getFeatures().sm()
1273 || (conversation.getMode() == Conversation.MODE_MULTI && message.getCounterpart().isBareJid())) {
1274 markMessage(message, Message.STATUS_UNSEND);
1275 } else {
1276 markMessage(message, Message.STATUS_SEND);
1277 }
1278 }
1279 } else {
1280 if (addToConversation) {
1281 conversation.add(message);
1282 }
1283 if (saveInDb) {
1284 databaseBackend.createMessage(message);
1285 } else if (message.edited()) {
1286 databaseBackend.updateMessage(message, message.getEditedId());
1287 }
1288 updateConversationUi();
1289 }
1290 if (packet != null) {
1291 if (delay) {
1292 mMessageGenerator.addDelay(packet, message.getTimeSent());
1293 }
1294 if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
1295 if (this.sendChatStates()) {
1296 packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
1297 }
1298 }
1299 sendMessagePacket(account, packet);
1300 }
1301 }
1302
1303 private void sendUnsentMessages(final Conversation conversation) {
1304 conversation.findWaitingMessages(new Conversation.OnMessageFound() {
1305
1306 @Override
1307 public void onMessageFound(Message message) {
1308 resendMessage(message, true);
1309 }
1310 });
1311 }
1312
1313 public void resendMessage(final Message message, final boolean delay) {
1314 sendMessage(message, true, delay);
1315 }
1316
1317 public void fetchRosterFromServer(final Account account) {
1318 final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
1319 if (!"".equals(account.getRosterVersion())) {
1320 Log.d(Config.LOGTAG, account.getJid().toBareJid()
1321 + ": fetching roster version " + account.getRosterVersion());
1322 } else {
1323 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
1324 }
1325 iqPacket.query(Namespace.ROSTER).setAttribute("ver", account.getRosterVersion());
1326 sendIqPacket(account, iqPacket, mIqParser);
1327 }
1328
1329 public void fetchBookmarks(final Account account) {
1330 final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
1331 final Element query = iqPacket.query("jabber:iq:private");
1332 query.addChild("storage", "storage:bookmarks");
1333 final OnIqPacketReceived callback = new OnIqPacketReceived() {
1334
1335 @Override
1336 public void onIqPacketReceived(final Account account, final IqPacket packet) {
1337 if (packet.getType() == IqPacket.TYPE.RESULT) {
1338 final Element query = packet.query();
1339 final HashMap<Jid, Bookmark> bookmarks = new HashMap<>();
1340 final Element storage = query.findChild("storage", "storage:bookmarks");
1341 final boolean autojoin = respectAutojoin();
1342 if (storage != null) {
1343 for (final Element item : storage.getChildren()) {
1344 if (item.getName().equals("conference")) {
1345 final Bookmark bookmark = Bookmark.parse(item, account);
1346 Bookmark old = bookmarks.put(bookmark.getJid(), bookmark);
1347 if (old != null && old.getBookmarkName() != null && bookmark.getBookmarkName() == null) {
1348 bookmark.setBookmarkName(old.getBookmarkName());
1349 }
1350 Conversation conversation = find(bookmark);
1351 if (conversation != null) {
1352 conversation.setBookmark(bookmark);
1353 } else if (bookmark.autojoin() && bookmark.getJid() != null && autojoin) {
1354 conversation = findOrCreateConversation(account, bookmark.getJid(), true, true, false);
1355 conversation.setBookmark(bookmark);
1356 }
1357 }
1358 }
1359 }
1360 account.setBookmarks(new ArrayList<>(bookmarks.values()));
1361 } else {
1362 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not fetch bookmarks");
1363 }
1364 }
1365 };
1366 sendIqPacket(account, iqPacket, callback);
1367 }
1368
1369 public void pushBookmarks(Account account) {
1370 Log.d(Config.LOGTAG, account.getJid().toBareJid()+": pushing bookmarks");
1371 IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
1372 Element query = iqPacket.query("jabber:iq:private");
1373 Element storage = query.addChild("storage", "storage:bookmarks");
1374 for (Bookmark bookmark : account.getBookmarks()) {
1375 storage.addChild(bookmark);
1376 }
1377 sendIqPacket(account, iqPacket, mDefaultIqHandler);
1378 }
1379
1380 private void restoreFromDatabase() {
1381 synchronized (this.conversations) {
1382 final Map<String, Account> accountLookupTable = new Hashtable<>();
1383 for (Account account : this.accounts) {
1384 accountLookupTable.put(account.getUuid(), account);
1385 }
1386 Log.d(Config.LOGTAG,"restoring conversations...");
1387 final long startTimeConversationsRestore = SystemClock.elapsedRealtime();
1388 this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
1389 for(Iterator<Conversation> iterator = conversations.listIterator(); iterator.hasNext();) {
1390 Conversation conversation = iterator.next();
1391 Account account = accountLookupTable.get(conversation.getAccountUuid());
1392 if (account != null) {
1393 conversation.setAccount(account);
1394 } else {
1395 Log.e(Config.LOGTAG,"unable to restore Conversations with "+conversation.getJid());
1396 iterator.remove();
1397 }
1398 }
1399 long diffConversationsRestore = SystemClock.elapsedRealtime() - startTimeConversationsRestore;
1400 Log.d(Config.LOGTAG,"finished restoring conversations in "+diffConversationsRestore+"ms");
1401 Runnable runnable = new Runnable() {
1402 @Override
1403 public void run() {
1404 long deletionDate = getAutomaticMessageDeletionDate();
1405 mLastExpiryRun.set(SystemClock.elapsedRealtime());
1406 if (deletionDate > 0) {
1407 Log.d(Config.LOGTAG, "deleting messages that are older than "+AbstractGenerator.getTimestamp(deletionDate));
1408 databaseBackend.expireOldMessages(deletionDate);
1409 }
1410 Log.d(Config.LOGTAG,"restoring roster...");
1411 for (Account account : accounts) {
1412 databaseBackend.readRoster(account.getRoster());
1413 account.initAccountServices(XmppConnectionService.this); //roster needs to be loaded at this stage
1414 }
1415 getBitmapCache().evictAll();
1416 loadPhoneContacts();
1417 Log.d(Config.LOGTAG, "restoring messages...");
1418 final long startMessageRestore = SystemClock.elapsedRealtime();
1419 for (Conversation conversation : conversations) {
1420 conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
1421 checkDeletedFiles(conversation);
1422 conversation.findUnsentTextMessages(new Conversation.OnMessageFound() {
1423
1424 @Override
1425 public void onMessageFound(Message message) {
1426 markMessage(message, Message.STATUS_WAITING);
1427 }
1428 });
1429 conversation.findUnreadMessages(new Conversation.OnMessageFound() {
1430 @Override
1431 public void onMessageFound(Message message) {
1432 mNotificationService.pushFromBacklog(message);
1433 }
1434 });
1435 }
1436 mNotificationService.finishBacklog(false);
1437 mRestoredFromDatabase = true;
1438 final long diffMessageRestore = SystemClock.elapsedRealtime() - startMessageRestore;
1439 Log.d(Config.LOGTAG, "finished restoring messages in "+diffMessageRestore+"ms");
1440 updateConversationUi();
1441 }
1442 };
1443 mDatabaseExecutor.execute(runnable);
1444 }
1445 }
1446
1447 public void loadPhoneContacts() {
1448 mContactMergerExecutor.execute(new Runnable() {
1449 @Override
1450 public void run() {
1451 PhoneHelper.loadPhoneContacts(XmppConnectionService.this, new OnPhoneContactsLoadedListener() {
1452 @Override
1453 public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
1454 Log.d(Config.LOGTAG, "start merging phone contacts with roster");
1455 for (Account account : accounts) {
1456 List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts();
1457 for (Bundle phoneContact : phoneContacts) {
1458 Jid jid;
1459 try {
1460 jid = Jid.fromString(phoneContact.getString("jid"));
1461 } catch (final InvalidJidException e) {
1462 continue;
1463 }
1464 final Contact contact = account.getRoster().getContact(jid);
1465 String systemAccount = phoneContact.getInt("phoneid")
1466 + "#"
1467 + phoneContact.getString("lookup");
1468 contact.setSystemAccount(systemAccount);
1469 boolean needsCacheClean = contact.setPhotoUri(phoneContact.getString("photouri"));
1470 needsCacheClean |= contact.setSystemName(phoneContact.getString("displayname"));
1471 if (needsCacheClean) {
1472 getAvatarService().clear(contact);
1473 }
1474 withSystemAccounts.remove(contact);
1475 }
1476 for (Contact contact : withSystemAccounts) {
1477 contact.setSystemAccount(null);
1478 boolean needsCacheClean = contact.setPhotoUri(null);
1479 needsCacheClean |= contact.setSystemName(null);
1480 if (needsCacheClean) {
1481 getAvatarService().clear(contact);
1482 }
1483 }
1484 }
1485 Log.d(Config.LOGTAG, "finished merging phone contacts");
1486 mShortcutService.refresh(mInitialAddressbookSyncCompleted.compareAndSet(false,true));
1487 updateAccountUi();
1488 }
1489 });
1490 }
1491 });
1492 }
1493
1494 public List<Conversation> getConversations() {
1495 return this.conversations;
1496 }
1497
1498 private void checkDeletedFiles(Conversation conversation) {
1499 conversation.findMessagesWithFiles(new Conversation.OnMessageFound() {
1500
1501 @Override
1502 public void onMessageFound(Message message) {
1503 if (!getFileBackend().isFileAvailable(message)) {
1504 message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
1505 final int s = message.getStatus();
1506 if (s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) {
1507 markMessage(message, Message.STATUS_SEND_FAILED);
1508 }
1509 }
1510 }
1511 });
1512 }
1513
1514 private void markFileDeleted(final String path) {
1515 Log.d(Config.LOGTAG,"deleted file "+path);
1516 for (Conversation conversation : getConversations()) {
1517 conversation.findMessagesWithFiles(new Conversation.OnMessageFound() {
1518 @Override
1519 public void onMessageFound(Message message) {
1520 DownloadableFile file = fileBackend.getFile(message);
1521 if (file.getAbsolutePath().equals(path)) {
1522 if (!file.exists()) {
1523 message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
1524 final int s = message.getStatus();
1525 if (s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) {
1526 markMessage(message, Message.STATUS_SEND_FAILED);
1527 } else {
1528 updateConversationUi();
1529 }
1530 } else {
1531 Log.d(Config.LOGTAG,"found matching message for file "+path+" but file still exists");
1532 }
1533 }
1534 }
1535 });
1536 }
1537 }
1538
1539 public void populateWithOrderedConversations(final List<Conversation> list) {
1540 populateWithOrderedConversations(list, true);
1541 }
1542
1543 public void populateWithOrderedConversations(final List<Conversation> list, boolean includeNoFileUpload) {
1544 list.clear();
1545 if (includeNoFileUpload) {
1546 list.addAll(getConversations());
1547 } else {
1548 for (Conversation conversation : getConversations()) {
1549 if (conversation.getMode() == Conversation.MODE_SINGLE
1550 || conversation.getAccount().httpUploadAvailable()) {
1551 list.add(conversation);
1552 }
1553 }
1554 }
1555 try {
1556 Collections.sort(list);
1557 } catch (IllegalArgumentException e) {
1558 //ignore
1559 }
1560 }
1561
1562 public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
1563 if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback)) {
1564 return;
1565 } else if (timestamp == 0) {
1566 return;
1567 }
1568 Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp));
1569 Runnable runnable = new Runnable() {
1570 @Override
1571 public void run() {
1572 final Account account = conversation.getAccount();
1573 List<Message> messages = databaseBackend.getMessages(conversation, 50, timestamp);
1574 if (messages.size() > 0) {
1575 conversation.addAll(0, messages);
1576 checkDeletedFiles(conversation);
1577 callback.onMoreMessagesLoaded(messages.size(), conversation);
1578 } else if (conversation.hasMessagesLeftOnServer()
1579 && account.isOnlineAndConnected()
1580 && conversation.getLastClearHistory().getTimestamp() == 0) {
1581 if ((conversation.getMode() == Conversation.MODE_SINGLE && account.getXmppConnection().getFeatures().mam())
1582 || (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().mamSupport())) {
1583 MessageArchiveService.Query query = getMessageArchiveService().query(conversation, new MamReference(0), timestamp, false);
1584 if (query != null) {
1585 query.setCallback(callback);
1586 callback.informUser(R.string.fetching_history_from_server);
1587 } else {
1588 callback.informUser(R.string.not_fetching_history_retention_period);
1589 }
1590
1591 }
1592 }
1593 }
1594 };
1595 mDatabaseExecutor.execute(runnable);
1596 }
1597
1598 public List<Account> getAccounts() {
1599 return this.accounts;
1600 }
1601
1602 public List<Conversation> findAllConferencesWith(Contact contact) {
1603 ArrayList<Conversation> results = new ArrayList<>();
1604 for(Conversation conversation : conversations) {
1605 if (conversation.getMode() == Conversation.MODE_MULTI
1606 && conversation.getMucOptions().isContactInRoom(contact)) {
1607 results.add(conversation);
1608 }
1609 }
1610 return results;
1611 }
1612
1613 public Conversation find(final Iterable<Conversation> haystack, final Contact contact) {
1614 for (final Conversation conversation : haystack) {
1615 if (conversation.getContact() == contact) {
1616 return conversation;
1617 }
1618 }
1619 return null;
1620 }
1621
1622 public Conversation find(final Iterable<Conversation> haystack, final Account account, final Jid jid) {
1623 if (jid == null) {
1624 return null;
1625 }
1626 for (final Conversation conversation : haystack) {
1627 if ((account == null || conversation.getAccount() == account)
1628 && (conversation.getJid().toBareJid().equals(jid.toBareJid()))) {
1629 return conversation;
1630 }
1631 }
1632 return null;
1633 }
1634
1635 public Conversation findOrCreateConversation(Account account, Jid jid, boolean muc, final boolean async) {
1636 return this.findOrCreateConversation(account,jid,muc,false, async);
1637 }
1638
1639 public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final boolean async) {
1640 return this.findOrCreateConversation(account, jid, muc, joinAfterCreate, null, async);
1641 }
1642
1643 public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final boolean joinAfterCreate, final MessageArchiveService.Query query, final boolean async) {
1644 synchronized (this.conversations) {
1645 Conversation conversation = find(account, jid);
1646 if (conversation != null) {
1647 return conversation;
1648 }
1649 conversation = databaseBackend.findConversation(account, jid);
1650 final boolean loadMessagesFromDb;
1651 if (conversation != null) {
1652 conversation.setStatus(Conversation.STATUS_AVAILABLE);
1653 conversation.setAccount(account);
1654 if (muc) {
1655 conversation.setMode(Conversation.MODE_MULTI);
1656 conversation.setContactJid(jid);
1657 } else {
1658 conversation.setMode(Conversation.MODE_SINGLE);
1659 conversation.setContactJid(jid.toBareJid());
1660 }
1661 databaseBackend.updateConversation(conversation);
1662 loadMessagesFromDb = conversation.messagesLoaded.compareAndSet(true,false);
1663 } else {
1664 String conversationName;
1665 Contact contact = account.getRoster().getContact(jid);
1666 if (contact != null) {
1667 conversationName = contact.getDisplayName();
1668 } else {
1669 conversationName = jid.getLocalpart();
1670 }
1671 if (muc) {
1672 conversation = new Conversation(conversationName, account, jid,
1673 Conversation.MODE_MULTI);
1674 } else {
1675 conversation = new Conversation(conversationName, account, jid.toBareJid(),
1676 Conversation.MODE_SINGLE);
1677 }
1678 this.databaseBackend.createConversation(conversation);
1679 loadMessagesFromDb = false;
1680 }
1681 final Conversation c = conversation;
1682 final Runnable runnable = new Runnable() {
1683 @Override
1684 public void run() {
1685 if (loadMessagesFromDb) {
1686 c.addAll(0, databaseBackend.getMessages(c, Config.PAGE_SIZE));
1687 updateConversationUi();
1688 c.messagesLoaded.set(true);
1689 }
1690 if (account.getXmppConnection() != null
1691 && account.getXmppConnection().getFeatures().mam()
1692 && !muc) {
1693 if (query == null) {
1694 mMessageArchiveService.query(c);
1695 } else {
1696 if (query.getConversation() == null) {
1697 mMessageArchiveService.query(c, query.getStart(),query.isCatchup());
1698 }
1699 }
1700 }
1701 checkDeletedFiles(c);
1702 if (joinAfterCreate) {
1703 joinMuc(c);
1704 }
1705 }
1706 };
1707 if (async) {
1708 mDatabaseExecutor.execute(runnable);
1709 } else {
1710 runnable.run();
1711 }
1712 this.conversations.add(conversation);
1713 updateConversationUi();
1714 return conversation;
1715 }
1716 }
1717
1718 public void archiveConversation(Conversation conversation) {
1719 getNotificationService().clear(conversation);
1720 conversation.setStatus(Conversation.STATUS_ARCHIVED);
1721 synchronized (this.conversations) {
1722 if (conversation.getMode() == Conversation.MODE_MULTI) {
1723 if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1724 Bookmark bookmark = conversation.getBookmark();
1725 if (bookmark != null && bookmark.autojoin() && respectAutojoin()) {
1726 bookmark.setAutojoin(false);
1727 pushBookmarks(bookmark.getAccount());
1728 }
1729 }
1730 leaveMuc(conversation);
1731 } else {
1732 conversation.endOtrIfNeeded();
1733 if (conversation.getContact().getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1734 Log.d(Config.LOGTAG, "Canceling presence request from " + conversation.getJid().toString());
1735 sendPresencePacket(
1736 conversation.getAccount(),
1737 mPresenceGenerator.stopPresenceUpdatesTo(conversation.getContact())
1738 );
1739 }
1740 }
1741 updateConversation(conversation);
1742 this.conversations.remove(conversation);
1743 updateConversationUi();
1744 }
1745 }
1746
1747 public void createAccount(final Account account) {
1748 account.initAccountServices(this);
1749 databaseBackend.createAccount(account);
1750 this.accounts.add(account);
1751 this.reconnectAccountInBackground(account);
1752 updateAccountUi();
1753 toggleForegroundService();
1754 }
1755
1756 public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
1757 new Thread(new Runnable() {
1758 @Override
1759 public void run() {
1760 try {
1761 X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
1762 Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
1763 if (info == null) {
1764 callback.informUser(R.string.certificate_does_not_contain_jid);
1765 return;
1766 }
1767 if (findAccountByJid(info.first) == null) {
1768 Account account = new Account(info.first, "");
1769 account.setPrivateKeyAlias(alias);
1770 account.setOption(Account.OPTION_DISABLED, true);
1771 account.setDisplayName(info.second);
1772 createAccount(account);
1773 callback.onAccountCreated(account);
1774 if (Config.X509_VERIFICATION) {
1775 try {
1776 getMemorizingTrustManager().getNonInteractive(account.getJid().getDomainpart()).checkClientTrusted(chain, "RSA");
1777 } catch (CertificateException e) {
1778 callback.informUser(R.string.certificate_chain_is_not_trusted);
1779 }
1780 }
1781 } else {
1782 callback.informUser(R.string.account_already_exists);
1783 }
1784 } catch (Exception e) {
1785 e.printStackTrace();
1786 callback.informUser(R.string.unable_to_parse_certificate);
1787 }
1788 }
1789 }).start();
1790
1791 }
1792
1793 public void updateKeyInAccount(final Account account, final String alias) {
1794 Log.d(Config.LOGTAG, account.getJid().toBareJid()+": update key in account " + alias);
1795 try {
1796 X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
1797 Log.d(Config.LOGTAG,account.getJid().toBareJid()+" loaded certificate chain");
1798 Pair<Jid, String> info = CryptoHelper.extractJidAndName(chain[0]);
1799 if (account.getJid().toBareJid().equals(info.first)) {
1800 account.setPrivateKeyAlias(alias);
1801 account.setDisplayName(info.second);
1802 databaseBackend.updateAccount(account);
1803 if (Config.X509_VERIFICATION) {
1804 try {
1805 getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
1806 } catch (CertificateException e) {
1807 showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
1808 }
1809 account.getAxolotlService().regenerateKeys(true);
1810 }
1811 } else {
1812 showErrorToastInUi(R.string.jid_does_not_match_certificate);
1813 }
1814 } catch (Exception e) {
1815 e.printStackTrace();
1816 }
1817 }
1818
1819 public boolean updateAccount(final Account account) {
1820 if (databaseBackend.updateAccount(account)) {
1821 account.setShowErrorNotification(true);
1822 this.statusListener.onStatusChanged(account);
1823 databaseBackend.updateAccount(account);
1824 reconnectAccountInBackground(account);
1825 updateAccountUi();
1826 getNotificationService().updateErrorNotification();
1827 toggleForegroundService();
1828 return true;
1829 } else {
1830 return false;
1831 }
1832 }
1833
1834 public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) {
1835 final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword);
1836 sendIqPacket(account, iq, new OnIqPacketReceived() {
1837 @Override
1838 public void onIqPacketReceived(final Account account, final IqPacket packet) {
1839 if (packet.getType() == IqPacket.TYPE.RESULT) {
1840 account.setPassword(newPassword);
1841 account.setOption(Account.OPTION_MAGIC_CREATE, false);
1842 databaseBackend.updateAccount(account);
1843 callback.onPasswordChangeSucceeded();
1844 } else {
1845 callback.onPasswordChangeFailed();
1846 }
1847 }
1848 });
1849 }
1850
1851 public void deleteAccount(final Account account) {
1852 synchronized (this.conversations) {
1853 for (final Conversation conversation : conversations) {
1854 if (conversation.getAccount() == account) {
1855 if (conversation.getMode() == Conversation.MODE_MULTI) {
1856 leaveMuc(conversation);
1857 } else if (conversation.getMode() == Conversation.MODE_SINGLE) {
1858 conversation.endOtrIfNeeded();
1859 }
1860 conversations.remove(conversation);
1861 }
1862 }
1863 if (account.getXmppConnection() != null) {
1864 new Thread(new Runnable() {
1865 @Override
1866 public void run() {
1867 disconnect(account, true);
1868 }
1869 }).start();
1870 }
1871 Runnable runnable = new Runnable() {
1872 @Override
1873 public void run() {
1874 if (!databaseBackend.deleteAccount(account)) {
1875 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": unable to delete account");
1876 }
1877 }
1878 };
1879 mDatabaseExecutor.execute(runnable);
1880 this.accounts.remove(account);
1881 updateAccountUi();
1882 getNotificationService().updateErrorNotification();
1883 }
1884 }
1885
1886 public void setOnConversationListChangedListener(OnConversationUpdate listener) {
1887 synchronized (this) {
1888 this.mLastActivity = System.currentTimeMillis();
1889 if (checkListeners()) {
1890 switchToForeground();
1891 }
1892 this.mOnConversationUpdate = listener;
1893 this.mNotificationService.setIsInForeground(true);
1894 if (this.convChangedListenerCount < 2) {
1895 this.convChangedListenerCount++;
1896 }
1897 }
1898 }
1899
1900 public void removeOnConversationListChangedListener() {
1901 synchronized (this) {
1902 this.convChangedListenerCount--;
1903 if (this.convChangedListenerCount <= 0) {
1904 this.convChangedListenerCount = 0;
1905 this.mOnConversationUpdate = null;
1906 this.mNotificationService.setIsInForeground(false);
1907 if (checkListeners()) {
1908 switchToBackground();
1909 }
1910 }
1911 }
1912 }
1913
1914 public void setOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
1915 synchronized (this) {
1916 if (checkListeners()) {
1917 switchToForeground();
1918 }
1919 this.mOnShowErrorToast = onShowErrorToast;
1920 if (this.showErrorToastListenerCount < 2) {
1921 this.showErrorToastListenerCount++;
1922 }
1923 }
1924 this.mOnShowErrorToast = onShowErrorToast;
1925 }
1926
1927 public void removeOnShowErrorToastListener() {
1928 synchronized (this) {
1929 this.showErrorToastListenerCount--;
1930 if (this.showErrorToastListenerCount <= 0) {
1931 this.showErrorToastListenerCount = 0;
1932 this.mOnShowErrorToast = null;
1933 if (checkListeners()) {
1934 switchToBackground();
1935 }
1936 }
1937 }
1938 }
1939
1940 public void setOnAccountListChangedListener(OnAccountUpdate listener) {
1941 synchronized (this) {
1942 if (checkListeners()) {
1943 switchToForeground();
1944 }
1945 this.mOnAccountUpdate = listener;
1946 if (this.accountChangedListenerCount < 2) {
1947 this.accountChangedListenerCount++;
1948 }
1949 }
1950 }
1951
1952 public void removeOnAccountListChangedListener() {
1953 synchronized (this) {
1954 this.accountChangedListenerCount--;
1955 if (this.accountChangedListenerCount <= 0) {
1956 this.mOnAccountUpdate = null;
1957 this.accountChangedListenerCount = 0;
1958 if (checkListeners()) {
1959 switchToBackground();
1960 }
1961 }
1962 }
1963 }
1964
1965 public void setOnCaptchaRequestedListener(OnCaptchaRequested listener) {
1966 synchronized (this) {
1967 if (checkListeners()) {
1968 switchToForeground();
1969 }
1970 this.mOnCaptchaRequested = listener;
1971 if (this.captchaRequestedListenerCount < 2) {
1972 this.captchaRequestedListenerCount++;
1973 }
1974 }
1975 }
1976
1977 public void removeOnCaptchaRequestedListener() {
1978 synchronized (this) {
1979 this.captchaRequestedListenerCount--;
1980 if (this.captchaRequestedListenerCount <= 0) {
1981 this.mOnCaptchaRequested = null;
1982 this.captchaRequestedListenerCount = 0;
1983 if (checkListeners()) {
1984 switchToBackground();
1985 }
1986 }
1987 }
1988 }
1989
1990 public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
1991 synchronized (this) {
1992 if (checkListeners()) {
1993 switchToForeground();
1994 }
1995 this.mOnRosterUpdate = listener;
1996 if (this.rosterChangedListenerCount < 2) {
1997 this.rosterChangedListenerCount++;
1998 }
1999 }
2000 }
2001
2002 public void removeOnRosterUpdateListener() {
2003 synchronized (this) {
2004 this.rosterChangedListenerCount--;
2005 if (this.rosterChangedListenerCount <= 0) {
2006 this.rosterChangedListenerCount = 0;
2007 this.mOnRosterUpdate = null;
2008 if (checkListeners()) {
2009 switchToBackground();
2010 }
2011 }
2012 }
2013 }
2014
2015 public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
2016 synchronized (this) {
2017 if (checkListeners()) {
2018 switchToForeground();
2019 }
2020 this.mOnUpdateBlocklist = listener;
2021 if (this.updateBlocklistListenerCount < 2) {
2022 this.updateBlocklistListenerCount++;
2023 }
2024 }
2025 }
2026
2027 public void removeOnUpdateBlocklistListener() {
2028 synchronized (this) {
2029 this.updateBlocklistListenerCount--;
2030 if (this.updateBlocklistListenerCount <= 0) {
2031 this.updateBlocklistListenerCount = 0;
2032 this.mOnUpdateBlocklist = null;
2033 if (checkListeners()) {
2034 switchToBackground();
2035 }
2036 }
2037 }
2038 }
2039
2040 public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
2041 synchronized (this) {
2042 if (checkListeners()) {
2043 switchToForeground();
2044 }
2045 this.mOnKeyStatusUpdated = listener;
2046 if (this.keyStatusUpdatedListenerCount < 2) {
2047 this.keyStatusUpdatedListenerCount++;
2048 }
2049 }
2050 }
2051
2052 public void removeOnNewKeysAvailableListener() {
2053 synchronized (this) {
2054 this.keyStatusUpdatedListenerCount--;
2055 if (this.keyStatusUpdatedListenerCount <= 0) {
2056 this.keyStatusUpdatedListenerCount = 0;
2057 this.mOnKeyStatusUpdated = null;
2058 if (checkListeners()) {
2059 switchToBackground();
2060 }
2061 }
2062 }
2063 }
2064
2065 public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
2066 synchronized (this) {
2067 if (checkListeners()) {
2068 switchToForeground();
2069 }
2070 this.mOnMucRosterUpdate = listener;
2071 if (this.mucRosterChangedListenerCount < 2) {
2072 this.mucRosterChangedListenerCount++;
2073 }
2074 }
2075 }
2076
2077 public void removeOnMucRosterUpdateListener() {
2078 synchronized (this) {
2079 this.mucRosterChangedListenerCount--;
2080 if (this.mucRosterChangedListenerCount <= 0) {
2081 this.mucRosterChangedListenerCount = 0;
2082 this.mOnMucRosterUpdate = null;
2083 if (checkListeners()) {
2084 switchToBackground();
2085 }
2086 }
2087 }
2088 }
2089
2090 public boolean checkListeners() {
2091 return (this.mOnAccountUpdate == null
2092 && this.mOnConversationUpdate == null
2093 && this.mOnRosterUpdate == null
2094 && this.mOnCaptchaRequested == null
2095 && this.mOnUpdateBlocklist == null
2096 && this.mOnShowErrorToast == null
2097 && this.mOnKeyStatusUpdated == null);
2098 }
2099
2100 private void switchToForeground() {
2101 final boolean broadcastLastActivity = broadcastLastActivity();
2102 for (Conversation conversation : getConversations()) {
2103 if (conversation.getMode() == Conversation.MODE_MULTI) {
2104 conversation.getMucOptions().resetChatState();
2105 } else {
2106 conversation.setIncomingChatState(Config.DEFAULT_CHATSTATE);
2107 }
2108 }
2109 for (Account account : getAccounts()) {
2110 if (account.getStatus() == Account.State.ONLINE) {
2111 account.deactivateGracePeriod();
2112 final XmppConnection connection = account.getXmppConnection();
2113 if (connection != null ) {
2114 if (connection.getFeatures().csi()) {
2115 connection.sendActive();
2116 }
2117 if (broadcastLastActivity) {
2118 sendPresence(account, false); //send new presence but don't include idle because we are not
2119 }
2120 }
2121 }
2122 }
2123 Log.d(Config.LOGTAG, "app switched into foreground");
2124 }
2125
2126 private void switchToBackground() {
2127 final boolean broadcastLastActivity = broadcastLastActivity();
2128 for (Account account : getAccounts()) {
2129 if (account.getStatus() == Account.State.ONLINE) {
2130 XmppConnection connection = account.getXmppConnection();
2131 if (connection != null) {
2132 if (broadcastLastActivity) {
2133 sendPresence(account, true);
2134 }
2135 if (connection.getFeatures().csi()) {
2136 connection.sendInactive();
2137 }
2138 }
2139 }
2140 }
2141 this.mNotificationService.setIsInForeground(false);
2142 Log.d(Config.LOGTAG, "app switched into background");
2143 }
2144
2145 private void connectMultiModeConversations(Account account) {
2146 List<Conversation> conversations = getConversations();
2147 for (Conversation conversation : conversations) {
2148 if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) {
2149 joinMuc(conversation);
2150 }
2151 }
2152 }
2153
2154 public void joinMuc(Conversation conversation) {
2155 joinMuc(conversation,null, false);
2156 }
2157
2158 public void joinMuc(Conversation conversation, boolean followedInvite) {
2159 joinMuc(conversation, null, followedInvite);
2160 }
2161
2162 private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
2163 joinMuc(conversation,onConferenceJoined,false);
2164 }
2165
2166 private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined, final boolean followedInvite) {
2167 Account account = conversation.getAccount();
2168 account.pendingConferenceJoins.remove(conversation);
2169 account.pendingConferenceLeaves.remove(conversation);
2170 if (account.getStatus() == Account.State.ONLINE) {
2171 sendPresencePacket(account, mPresenceGenerator.leave(conversation.getMucOptions()));
2172 conversation.resetMucOptions();
2173 if (onConferenceJoined != null) {
2174 conversation.getMucOptions().flagNoAutoPushConfiguration();
2175 }
2176 conversation.setHasMessagesLeftOnServer(false);
2177 fetchConferenceConfiguration(conversation, new OnConferenceConfigurationFetched() {
2178
2179 private void join(Conversation conversation) {
2180 Account account = conversation.getAccount();
2181 final MucOptions mucOptions = conversation.getMucOptions();
2182 final Jid joinJid = mucOptions.getSelf().getFullJid();
2183 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString());
2184 PresencePacket packet = mPresenceGenerator.selfPresence(account, Presence.Status.ONLINE, mucOptions.nonanonymous() || onConferenceJoined != null);
2185 packet.setTo(joinJid);
2186 Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
2187 if (conversation.getMucOptions().getPassword() != null) {
2188 x.addChild("password").setContent(mucOptions.getPassword());
2189 }
2190
2191 if (mucOptions.mamSupport()) {
2192 // Use MAM instead of the limited muc history to get history
2193 x.addChild("history").setAttribute("maxchars", "0");
2194 } else {
2195 // Fallback to muc history
2196 x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted().getTimestamp()));
2197 }
2198 sendPresencePacket(account, packet);
2199 if (onConferenceJoined != null) {
2200 onConferenceJoined.onConferenceJoined(conversation);
2201 }
2202 if (!joinJid.equals(conversation.getJid())) {
2203 conversation.setContactJid(joinJid);
2204 databaseBackend.updateConversation(conversation);
2205 }
2206
2207 if (mucOptions.mamSupport()) {
2208 getMessageArchiveService().catchupMUC(conversation);
2209 }
2210 if (mucOptions.membersOnly() && mucOptions.nonanonymous()) {
2211 fetchConferenceMembers(conversation);
2212 if (followedInvite && conversation.getBookmark() == null) {
2213 saveConversationAsBookmark(conversation,null);
2214 }
2215 }
2216 sendUnsentMessages(conversation);
2217 }
2218
2219 @Override
2220 public void onConferenceConfigurationFetched(Conversation conversation) {
2221 join(conversation);
2222 }
2223
2224 @Override
2225 public void onFetchFailed(final Conversation conversation, Element error) {
2226 if (error != null && "remote-server-not-found".equals(error.getName())) {
2227 conversation.getMucOptions().setError(MucOptions.Error.SERVER_NOT_FOUND);
2228 updateConversationUi();
2229 } else {
2230 join(conversation);
2231 fetchConferenceConfiguration(conversation);
2232 }
2233 }
2234 });
2235 updateConversationUi();
2236 } else {
2237 account.pendingConferenceJoins.add(conversation);
2238 conversation.resetMucOptions();
2239 conversation.setHasMessagesLeftOnServer(false);
2240 updateConversationUi();
2241 }
2242 }
2243
2244 private void fetchConferenceMembers(final Conversation conversation) {
2245 final Account account = conversation.getAccount();
2246 final AxolotlService axolotlService = account.getAxolotlService();
2247 final String[] affiliations = {"member","admin","owner"};
2248 OnIqPacketReceived callback = new OnIqPacketReceived() {
2249
2250 private int i = 0;
2251 private boolean success = true;
2252
2253 @Override
2254 public void onIqPacketReceived(Account account, IqPacket packet) {
2255
2256 Element query = packet.query("http://jabber.org/protocol/muc#admin");
2257 if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
2258 for(Element child : query.getChildren()) {
2259 if ("item".equals(child.getName())) {
2260 MucOptions.User user = AbstractParser.parseItem(conversation,child);
2261 if (!user.realJidMatchesAccount()) {
2262 boolean isNew = conversation.getMucOptions().updateUser(user);
2263 if (isNew && user.getRealJid() != null && axolotlService.hasEmptyDeviceList(user.getRealJid())) {
2264 axolotlService.fetchDeviceIds(user.getRealJid());
2265 }
2266 }
2267 }
2268 }
2269 } else {
2270 success = false;
2271 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not request affiliation "+affiliations[i]+" in "+conversation.getJid().toBareJid());
2272 }
2273 ++i;
2274 if (i >= affiliations.length) {
2275 List<Jid> members = conversation.getMucOptions().getMembers();
2276 if (success) {
2277 List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
2278 boolean changed = false;
2279 for(ListIterator<Jid> iterator = cryptoTargets.listIterator(); iterator.hasNext();) {
2280 Jid jid = iterator.next();
2281 if (!members.contains(jid)) {
2282 iterator.remove();
2283 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": removed "+jid+" from crypto targets of "+conversation.getName());
2284 changed = true;
2285 }
2286 }
2287 if (changed) {
2288 conversation.setAcceptedCryptoTargets(cryptoTargets);
2289 updateConversation(conversation);
2290 }
2291 }
2292 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved members for "+conversation.getJid().toBareJid()+": "+conversation.getMucOptions().getMembers());
2293 getAvatarService().clear(conversation);
2294 updateMucRosterUi();
2295 updateConversationUi();
2296 }
2297 }
2298 };
2299 for(String affiliation : affiliations) {
2300 sendIqPacket(account, mIqGenerator.queryAffiliation(conversation, affiliation), callback);
2301 }
2302 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetching members for "+conversation.getName());
2303 }
2304
2305 public void providePasswordForMuc(Conversation conversation, String password) {
2306 if (conversation.getMode() == Conversation.MODE_MULTI) {
2307 conversation.getMucOptions().setPassword(password);
2308 if (conversation.getBookmark() != null) {
2309 if (respectAutojoin()) {
2310 conversation.getBookmark().setAutojoin(true);
2311 }
2312 pushBookmarks(conversation.getAccount());
2313 }
2314 updateConversation(conversation);
2315 joinMuc(conversation);
2316 }
2317 }
2318
2319 private boolean hasEnabledAccounts() {
2320 for(Account account : this.accounts) {
2321 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
2322 return true;
2323 }
2324 }
2325 return false;
2326 }
2327
2328 public void renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
2329 final MucOptions options = conversation.getMucOptions();
2330 final Jid joinJid = options.createJoinJid(nick);
2331 if (options.online()) {
2332 Account account = conversation.getAccount();
2333 options.setOnRenameListener(new OnRenameListener() {
2334
2335 @Override
2336 public void onSuccess() {
2337 conversation.setContactJid(joinJid);
2338 databaseBackend.updateConversation(conversation);
2339 Bookmark bookmark = conversation.getBookmark();
2340 if (bookmark != null) {
2341 bookmark.setNick(nick);
2342 pushBookmarks(bookmark.getAccount());
2343 }
2344 callback.success(conversation);
2345 }
2346
2347 @Override
2348 public void onFailure() {
2349 callback.error(R.string.nick_in_use, conversation);
2350 }
2351 });
2352
2353 PresencePacket packet = new PresencePacket();
2354 packet.setTo(joinJid);
2355 packet.setFrom(conversation.getAccount().getJid());
2356
2357 String sig = account.getPgpSignature();
2358 if (sig != null) {
2359 packet.addChild("status").setContent("online");
2360 packet.addChild("x", "jabber:x:signed").setContent(sig);
2361 }
2362 sendPresencePacket(account, packet);
2363 } else {
2364 conversation.setContactJid(joinJid);
2365 databaseBackend.updateConversation(conversation);
2366 if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
2367 Bookmark bookmark = conversation.getBookmark();
2368 if (bookmark != null) {
2369 bookmark.setNick(nick);
2370 pushBookmarks(bookmark.getAccount());
2371 }
2372 joinMuc(conversation);
2373 }
2374 }
2375 }
2376
2377 public void leaveMuc(Conversation conversation) {
2378 leaveMuc(conversation, false);
2379 }
2380
2381 private void leaveMuc(Conversation conversation, boolean now) {
2382 Account account = conversation.getAccount();
2383 account.pendingConferenceJoins.remove(conversation);
2384 account.pendingConferenceLeaves.remove(conversation);
2385 if (account.getStatus() == Account.State.ONLINE || now) {
2386 sendPresencePacket(conversation.getAccount(), mPresenceGenerator.leave(conversation.getMucOptions()));
2387 conversation.getMucOptions().setOffline();
2388 conversation.deregisterWithBookmark();
2389 Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": leaving muc " + conversation.getJid());
2390 } else {
2391 account.pendingConferenceLeaves.add(conversation);
2392 }
2393 }
2394
2395 public String findConferenceServer(final Account account) {
2396 String server;
2397 if (account.getXmppConnection() != null) {
2398 server = account.getXmppConnection().getMucServer();
2399 if (server != null) {
2400 return server;
2401 }
2402 }
2403 for (Account other : getAccounts()) {
2404 if (other != account && other.getXmppConnection() != null) {
2405 server = other.getXmppConnection().getMucServer();
2406 if (server != null) {
2407 return server;
2408 }
2409 }
2410 }
2411 return null;
2412 }
2413
2414 public boolean createAdhocConference(final Account account,
2415 final String subject,
2416 final Iterable<Jid> jids,
2417 final UiCallback<Conversation> callback) {
2418 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString());
2419 if (account.getStatus() == Account.State.ONLINE) {
2420 try {
2421 String server = findConferenceServer(account);
2422 if (server == null) {
2423 if (callback != null) {
2424 callback.error(R.string.no_conference_server_found, null);
2425 }
2426 return false;
2427 }
2428 final Jid jid = Jid.fromParts(new BigInteger(64, getRNG()).toString(Character.MAX_RADIX), server, null);
2429 final Conversation conversation = findOrCreateConversation(account, jid, true, false, true);
2430 joinMuc(conversation, new OnConferenceJoined() {
2431 @Override
2432 public void onConferenceJoined(final Conversation conversation) {
2433 pushConferenceConfiguration(conversation, IqGenerator.defaultRoomConfiguration(), new OnConfigurationPushed() {
2434 @Override
2435 public void onPushSucceeded() {
2436 if (subject != null && !subject.trim().isEmpty()) {
2437 pushSubjectToConference(conversation, subject.trim());
2438 }
2439 for (Jid invite : jids) {
2440 invite(conversation, invite);
2441 }
2442 if (account.countPresences() > 1) {
2443 directInvite(conversation, account.getJid().toBareJid());
2444 }
2445 saveConversationAsBookmark(conversation, subject);
2446 if (callback != null) {
2447 callback.success(conversation);
2448 }
2449 }
2450
2451 @Override
2452 public void onPushFailed() {
2453 archiveConversation(conversation);
2454 if (callback != null) {
2455 callback.error(R.string.conference_creation_failed, conversation);
2456 }
2457 }
2458 });
2459 }
2460 });
2461 return true;
2462 } catch (InvalidJidException e) {
2463 if (callback != null) {
2464 callback.error(R.string.conference_creation_failed, null);
2465 }
2466 return false;
2467 }
2468 } else {
2469 if (callback != null) {
2470 callback.error(R.string.not_connected_try_again, null);
2471 }
2472 return false;
2473 }
2474 }
2475
2476 public void fetchConferenceConfiguration(final Conversation conversation) {
2477 fetchConferenceConfiguration(conversation, null);
2478 }
2479
2480 public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) {
2481 IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2482 request.setTo(conversation.getJid().toBareJid());
2483 request.query("http://jabber.org/protocol/disco#info");
2484 sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
2485 @Override
2486 public void onIqPacketReceived(Account account, IqPacket packet) {
2487 Element query = packet.findChild("query","http://jabber.org/protocol/disco#info");
2488 if (packet.getType() == IqPacket.TYPE.RESULT && query != null) {
2489 ArrayList<String> features = new ArrayList<>();
2490 for (Element child : query.getChildren()) {
2491 if (child != null && child.getName().equals("feature")) {
2492 String var = child.getAttribute("var");
2493 if (var != null) {
2494 features.add(var);
2495 }
2496 }
2497 }
2498 Element form = query.findChild("x", "jabber:x:data");
2499 if (form != null) {
2500 conversation.getMucOptions().updateFormData(Data.parse(form));
2501 }
2502 conversation.getMucOptions().updateFeatures(features);
2503 if (callback != null) {
2504 callback.onConferenceConfigurationFetched(conversation);
2505 }
2506 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": fetched muc configuration for "+conversation.getJid().toBareJid()+" - "+features.toString());
2507 updateConversationUi();
2508 } else if (packet.getType() == IqPacket.TYPE.ERROR) {
2509 if (callback != null) {
2510 callback.onFetchFailed(conversation, packet.getError());
2511 }
2512 }
2513 }
2514 });
2515 }
2516
2517 public void pushNodeConfiguration(Account account, final String node, final Bundle options, final OnConfigurationPushed callback) {
2518 pushNodeConfiguration(account,account.getJid().toBareJid(),node,options,callback);
2519 }
2520
2521 public void pushNodeConfiguration(Account account, final Jid jid, final String node, final Bundle options, final OnConfigurationPushed callback) {
2522 sendIqPacket(account, mIqGenerator.requestPubsubConfiguration(jid,node), new OnIqPacketReceived() {
2523 @Override
2524 public void onIqPacketReceived(Account account, IqPacket packet) {
2525 if (packet.getType() == IqPacket.TYPE.RESULT) {
2526 Element pubsub = packet.findChild("pubsub","http://jabber.org/protocol/pubsub#owner");
2527 Element configuration = pubsub == null ? null : pubsub.findChild("configure");
2528 Element x = configuration == null ? null : configuration.findChild("x","jabber:x:data");
2529 if (x != null) {
2530 Data data = Data.parse(x);
2531 data.submit(options);
2532 sendIqPacket(account, mIqGenerator.publishPubsubConfiguration(jid, node, data), new OnIqPacketReceived() {
2533 @Override
2534 public void onIqPacketReceived(Account account, IqPacket packet) {
2535 if (packet.getType() == IqPacket.TYPE.RESULT && callback != null) {
2536 callback.onPushSucceeded();
2537 } else {
2538 Log.d(Config.LOGTAG,packet.toString());
2539 }
2540 }
2541 });
2542 } else if (callback !=null) {
2543 callback.onPushFailed();
2544 }
2545 } else if (callback != null){
2546 callback.onPushFailed();
2547 }
2548 }
2549 });
2550 }
2551
2552 public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConfigurationPushed callback) {
2553 IqPacket request = new IqPacket(IqPacket.TYPE.GET);
2554 request.setTo(conversation.getJid().toBareJid());
2555 request.query("http://jabber.org/protocol/muc#owner");
2556 sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
2557 @Override
2558 public void onIqPacketReceived(Account account, IqPacket packet) {
2559 if (packet.getType() == IqPacket.TYPE.RESULT) {
2560 Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
2561 data.submit(options);
2562 IqPacket set = new IqPacket(IqPacket.TYPE.SET);
2563 set.setTo(conversation.getJid().toBareJid());
2564 set.query("http://jabber.org/protocol/muc#owner").addChild(data);
2565 sendIqPacket(account, set, new OnIqPacketReceived() {
2566 @Override
2567 public void onIqPacketReceived(Account account, IqPacket packet) {
2568 if (callback != null) {
2569 if (packet.getType() == IqPacket.TYPE.RESULT) {
2570 callback.onPushSucceeded();
2571 } else {
2572 callback.onPushFailed();
2573 }
2574 }
2575 }
2576 });
2577 } else {
2578 if (callback != null) {
2579 callback.onPushFailed();
2580 }
2581 }
2582 }
2583 });
2584 }
2585
2586 public void pushSubjectToConference(final Conversation conference, final String subject) {
2587 MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, subject);
2588 this.sendMessagePacket(conference.getAccount(), packet);
2589 final MucOptions mucOptions = conference.getMucOptions();
2590 final MucOptions.User self = mucOptions.getSelf();
2591 if (!mucOptions.persistent() && self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
2592 Bundle options = new Bundle();
2593 options.putString("muc#roomconfig_persistentroom", "1");
2594 this.pushConferenceConfiguration(conference, options, null);
2595 }
2596 }
2597
2598 public void changeAffiliationInConference(final Conversation conference, Jid user, final MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
2599 final Jid jid = user.toBareJid();
2600 IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
2601 sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
2602 @Override
2603 public void onIqPacketReceived(Account account, IqPacket packet) {
2604 if (packet.getType() == IqPacket.TYPE.RESULT) {
2605 conference.getMucOptions().changeAffiliation(jid, affiliation);
2606 getAvatarService().clear(conference);
2607 callback.onAffiliationChangedSuccessful(jid);
2608 } else {
2609 callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
2610 }
2611 }
2612 });
2613 }
2614
2615 public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
2616 List<Jid> jids = new ArrayList<>();
2617 for (MucOptions.User user : conference.getMucOptions().getUsers()) {
2618 if (user.getAffiliation() == before && user.getRealJid() != null) {
2619 jids.add(user.getRealJid());
2620 }
2621 }
2622 IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
2623 sendIqPacket(conference.getAccount(), request, mDefaultIqHandler);
2624 }
2625
2626 public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) {
2627 IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
2628 Log.d(Config.LOGTAG, request.toString());
2629 sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
2630 @Override
2631 public void onIqPacketReceived(Account account, IqPacket packet) {
2632 Log.d(Config.LOGTAG, packet.toString());
2633 if (packet.getType() == IqPacket.TYPE.RESULT) {
2634 callback.onRoleChangedSuccessful(nick);
2635 } else {
2636 callback.onRoleChangeFailed(nick, R.string.could_not_change_role);
2637 }
2638 }
2639 });
2640 }
2641
2642 private void disconnect(Account account, boolean force) {
2643 if ((account.getStatus() == Account.State.ONLINE)
2644 || (account.getStatus() == Account.State.DISABLED)) {
2645 final XmppConnection connection = account.getXmppConnection();
2646 if (!force) {
2647 List<Conversation> conversations = getConversations();
2648 for (Conversation conversation : conversations) {
2649 if (conversation.getAccount() == account) {
2650 if (conversation.getMode() == Conversation.MODE_MULTI) {
2651 leaveMuc(conversation, true);
2652 } else {
2653 if (conversation.endOtrIfNeeded()) {
2654 Log.d(Config.LOGTAG, account.getJid().toBareJid()
2655 + ": ended otr session with "
2656 + conversation.getJid());
2657 }
2658 }
2659 }
2660 }
2661 sendOfflinePresence(account);
2662 }
2663 connection.disconnect(force);
2664 }
2665 }
2666
2667 @Override
2668 public IBinder onBind(Intent intent) {
2669 return mBinder;
2670 }
2671
2672 public void updateMessage(Message message) {
2673 databaseBackend.updateMessage(message);
2674 updateConversationUi();
2675 }
2676
2677 public void updateMessage(Message message, String uuid) {
2678 databaseBackend.updateMessage(message, uuid);
2679 updateConversationUi();
2680 }
2681
2682 protected void syncDirtyContacts(Account account) {
2683 for (Contact contact : account.getRoster().getContacts()) {
2684 if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
2685 pushContactToServer(contact);
2686 }
2687 if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
2688 deleteContactOnServer(contact);
2689 }
2690 }
2691 }
2692
2693 public void createContact(Contact contact) {
2694 boolean autoGrant = getBooleanPreference("grant_new_contacts", R.bool.grant_new_contacts);
2695 if (autoGrant) {
2696 contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
2697 contact.setOption(Contact.Options.ASKING);
2698 }
2699 pushContactToServer(contact);
2700 }
2701
2702 public void onOtrSessionEstablished(Conversation conversation) {
2703 final Account account = conversation.getAccount();
2704 final Session otrSession = conversation.getOtrSession();
2705 Log.d(Config.LOGTAG,
2706 account.getJid().toBareJid() + " otr session established with "
2707 + conversation.getJid() + "/"
2708 + otrSession.getSessionID().getUserID());
2709 conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
2710
2711 @Override
2712 public void onMessageFound(Message message) {
2713 SessionID id = otrSession.getSessionID();
2714 try {
2715 message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
2716 } catch (InvalidJidException e) {
2717 return;
2718 }
2719 if (message.needsUploading()) {
2720 mJingleConnectionManager.createNewConnection(message);
2721 } else {
2722 MessagePacket outPacket = mMessageGenerator.generateOtrChat(message);
2723 if (outPacket != null) {
2724 mMessageGenerator.addDelay(outPacket, message.getTimeSent());
2725 message.setStatus(Message.STATUS_SEND);
2726 databaseBackend.updateMessage(message);
2727 sendMessagePacket(account, outPacket);
2728 }
2729 }
2730 updateConversationUi();
2731 }
2732 });
2733 }
2734
2735 public boolean renewSymmetricKey(Conversation conversation) {
2736 Account account = conversation.getAccount();
2737 byte[] symmetricKey = new byte[32];
2738 this.mRandom.nextBytes(symmetricKey);
2739 Session otrSession = conversation.getOtrSession();
2740 if (otrSession != null) {
2741 MessagePacket packet = new MessagePacket();
2742 packet.setType(MessagePacket.TYPE_CHAT);
2743 packet.setFrom(account.getJid());
2744 MessageGenerator.addMessageHints(packet);
2745 packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
2746 + otrSession.getSessionID().getUserID());
2747 try {
2748 packet.setBody(otrSession
2749 .transformSending(CryptoHelper.FILETRANSFER
2750 + CryptoHelper.bytesToHex(symmetricKey))[0]);
2751 sendMessagePacket(account, packet);
2752 conversation.setSymmetricKey(symmetricKey);
2753 return true;
2754 } catch (OtrException e) {
2755 return false;
2756 }
2757 }
2758 return false;
2759 }
2760
2761 public void pushContactToServer(final Contact contact) {
2762 contact.resetOption(Contact.Options.DIRTY_DELETE);
2763 contact.setOption(Contact.Options.DIRTY_PUSH);
2764 final Account account = contact.getAccount();
2765 if (account.getStatus() == Account.State.ONLINE) {
2766 final boolean ask = contact.getOption(Contact.Options.ASKING);
2767 final boolean sendUpdates = contact
2768 .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
2769 && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
2770 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2771 iq.query(Namespace.ROSTER).addChild(contact.asElement());
2772 account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
2773 if (sendUpdates) {
2774 sendPresencePacket(account,
2775 mPresenceGenerator.sendPresenceUpdatesTo(contact));
2776 }
2777 if (ask) {
2778 sendPresencePacket(account,
2779 mPresenceGenerator.requestPresenceUpdatesFrom(contact));
2780 }
2781 }
2782 }
2783
2784 public void publishAvatar(Account account, Uri image, UiCallback<Avatar> callback) {
2785 final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
2786 final int size = Config.AVATAR_SIZE;
2787 final Avatar avatar = getFileBackend().getPepAvatar(image, size, format);
2788 if (avatar != null) {
2789 avatar.height = size;
2790 avatar.width = size;
2791 if (format.equals(Bitmap.CompressFormat.WEBP)) {
2792 avatar.type = "image/webp";
2793 } else if (format.equals(Bitmap.CompressFormat.JPEG)) {
2794 avatar.type = "image/jpeg";
2795 } else if (format.equals(Bitmap.CompressFormat.PNG)) {
2796 avatar.type = "image/png";
2797 }
2798 if (!getFileBackend().save(avatar)) {
2799 callback.error(R.string.error_saving_avatar, avatar);
2800 return;
2801 }
2802 publishAvatar(account, avatar, callback);
2803 } else {
2804 callback.error(R.string.error_publish_avatar_converting, null);
2805 }
2806 }
2807
2808 public void publishAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2809 IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
2810 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2811
2812 @Override
2813 public void onIqPacketReceived(Account account, IqPacket result) {
2814 if (result.getType() == IqPacket.TYPE.RESULT) {
2815 final IqPacket packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar);
2816 sendIqPacket(account, packet, new OnIqPacketReceived() {
2817 @Override
2818 public void onIqPacketReceived(Account account, IqPacket result) {
2819 if (result.getType() == IqPacket.TYPE.RESULT) {
2820 if (account.setAvatar(avatar.getFilename())) {
2821 getAvatarService().clear(account);
2822 databaseBackend.updateAccount(account);
2823 }
2824 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": published avatar "+(avatar.size/1024)+"KiB");
2825 if (callback != null) {
2826 callback.success(avatar);
2827 }
2828 } else {
2829 if (callback != null) {
2830 callback.error(R.string.error_publish_avatar_server_reject,avatar);
2831 }
2832 }
2833 }
2834 });
2835 } else {
2836 Element error = result.findChild("error");
2837 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server rejected avatar "+(avatar.size/1024)+"KiB "+(error!=null?error.toString():""));
2838 if (callback != null) {
2839 callback.error(R.string.error_publish_avatar_server_reject, avatar);
2840 }
2841 }
2842 }
2843 });
2844 }
2845
2846 public void republishAvatarIfNeeded(Account account) {
2847 if (account.getAxolotlService().isPepBroken()) {
2848 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping republication of avatar because pep is broken");
2849 return;
2850 }
2851 IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
2852 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2853
2854 private Avatar parseAvatar(IqPacket packet) {
2855 Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub");
2856 if (pubsub != null) {
2857 Element items = pubsub.findChild("items");
2858 if (items != null) {
2859 return Avatar.parseMetadata(items);
2860 }
2861 }
2862 return null;
2863 }
2864
2865 private boolean errorIsItemNotFound(IqPacket packet) {
2866 Element error = packet.findChild("error");
2867 return packet.getType() == IqPacket.TYPE.ERROR
2868 && error != null
2869 && error.hasChild("item-not-found");
2870 }
2871
2872 @Override
2873 public void onIqPacketReceived(Account account, IqPacket packet) {
2874 if (packet.getType() == IqPacket.TYPE.RESULT || errorIsItemNotFound(packet)) {
2875 Avatar serverAvatar = parseAvatar(packet);
2876 if (serverAvatar == null && account.getAvatar() != null) {
2877 Avatar avatar = fileBackend.getStoredPepAvatar(account.getAvatar());
2878 if (avatar != null) {
2879 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": avatar on server was null. republishing");
2880 publishAvatar(account, fileBackend.getStoredPepAvatar(account.getAvatar()), null);
2881 } else {
2882 Log.e(Config.LOGTAG, account.getJid().toBareJid()+": error rereading avatar");
2883 }
2884 }
2885 }
2886 }
2887 });
2888 }
2889
2890 public void fetchAvatar(Account account, Avatar avatar) {
2891 fetchAvatar(account, avatar, null);
2892 }
2893
2894 public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2895 final String KEY = generateFetchKey(account, avatar);
2896 synchronized (this.mInProgressAvatarFetches) {
2897 if (!this.mInProgressAvatarFetches.contains(KEY)) {
2898 switch (avatar.origin) {
2899 case PEP:
2900 this.mInProgressAvatarFetches.add(KEY);
2901 fetchAvatarPep(account, avatar, callback);
2902 break;
2903 case VCARD:
2904 this.mInProgressAvatarFetches.add(KEY);
2905 fetchAvatarVcard(account, avatar, callback);
2906 break;
2907 }
2908 }
2909 }
2910 }
2911
2912 private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2913 IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar);
2914 sendIqPacket(account, packet, new OnIqPacketReceived() {
2915
2916 @Override
2917 public void onIqPacketReceived(Account account, IqPacket result) {
2918 synchronized (mInProgressAvatarFetches) {
2919 mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
2920 }
2921 final String ERROR = account.getJid().toBareJid()
2922 + ": fetching avatar for " + avatar.owner + " failed ";
2923 if (result.getType() == IqPacket.TYPE.RESULT) {
2924 avatar.image = mIqParser.avatarData(result);
2925 if (avatar.image != null) {
2926 if (getFileBackend().save(avatar)) {
2927 if (account.getJid().toBareJid().equals(avatar.owner)) {
2928 if (account.setAvatar(avatar.getFilename())) {
2929 databaseBackend.updateAccount(account);
2930 }
2931 getAvatarService().clear(account);
2932 updateConversationUi();
2933 updateAccountUi();
2934 } else {
2935 Contact contact = account.getRoster()
2936 .getContact(avatar.owner);
2937 contact.setAvatar(avatar);
2938 getAvatarService().clear(contact);
2939 updateConversationUi();
2940 updateRosterUi();
2941 }
2942 if (callback != null) {
2943 callback.success(avatar);
2944 }
2945 Log.d(Config.LOGTAG, account.getJid().toBareJid()
2946 + ": successfully fetched pep avatar for " + avatar.owner);
2947 return;
2948 }
2949 } else {
2950
2951 Log.d(Config.LOGTAG, ERROR + "(parsing error)");
2952 }
2953 } else {
2954 Element error = result.findChild("error");
2955 if (error == null) {
2956 Log.d(Config.LOGTAG, ERROR + "(server error)");
2957 } else {
2958 Log.d(Config.LOGTAG, ERROR + error.toString());
2959 }
2960 }
2961 if (callback != null) {
2962 callback.error(0, null);
2963 }
2964
2965 }
2966 });
2967 }
2968
2969 private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2970 IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
2971 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2972 @Override
2973 public void onIqPacketReceived(Account account, IqPacket packet) {
2974 synchronized (mInProgressAvatarFetches) {
2975 mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
2976 }
2977 if (packet.getType() == IqPacket.TYPE.RESULT) {
2978 Element vCard = packet.findChild("vCard", "vcard-temp");
2979 Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
2980 String image = photo != null ? photo.findChildContent("BINVAL") : null;
2981 if (image != null) {
2982 avatar.image = image;
2983 if (getFileBackend().save(avatar)) {
2984 Log.d(Config.LOGTAG, account.getJid().toBareJid()
2985 + ": successfully fetched vCard avatar for " + avatar.owner);
2986 if (avatar.owner.isBareJid()) {
2987 if (account.getJid().toBareJid().equals(avatar.owner) && account.getAvatar() == null) {
2988 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": had no avatar. replacing with vcard");
2989 account.setAvatar(avatar.getFilename());
2990 databaseBackend.updateAccount(account);
2991 getAvatarService().clear(account);
2992 updateAccountUi();
2993 } else {
2994 Contact contact = account.getRoster().getContact(avatar.owner);
2995 contact.setAvatar(avatar);
2996 getAvatarService().clear(contact);
2997 updateRosterUi();
2998 }
2999 updateConversationUi();
3000 } else {
3001 Conversation conversation = find(account, avatar.owner.toBareJid());
3002 if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
3003 MucOptions.User user = conversation.getMucOptions().findUserByFullJid(avatar.owner);
3004 if (user != null) {
3005 if (user.setAvatar(avatar)) {
3006 getAvatarService().clear(user);
3007 updateConversationUi();
3008 updateMucRosterUi();
3009 }
3010 }
3011 }
3012 }
3013 }
3014 }
3015 }
3016 }
3017 });
3018 }
3019
3020 public void checkForAvatar(Account account, final UiCallback<Avatar> callback) {
3021 IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
3022 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
3023
3024 @Override
3025 public void onIqPacketReceived(Account account, IqPacket packet) {
3026 if (packet.getType() == IqPacket.TYPE.RESULT) {
3027 Element pubsub = packet.findChild("pubsub","http://jabber.org/protocol/pubsub");
3028 if (pubsub != null) {
3029 Element items = pubsub.findChild("items");
3030 if (items != null) {
3031 Avatar avatar = Avatar.parseMetadata(items);
3032 if (avatar != null) {
3033 avatar.owner = account.getJid().toBareJid();
3034 if (fileBackend.isAvatarCached(avatar)) {
3035 if (account.setAvatar(avatar.getFilename())) {
3036 databaseBackend.updateAccount(account);
3037 }
3038 getAvatarService().clear(account);
3039 callback.success(avatar);
3040 } else {
3041 fetchAvatarPep(account, avatar, callback);
3042 }
3043 return;
3044 }
3045 }
3046 }
3047 }
3048 callback.error(0, null);
3049 }
3050 });
3051 }
3052
3053 public void deleteContactOnServer(Contact contact) {
3054 contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
3055 contact.resetOption(Contact.Options.DIRTY_PUSH);
3056 contact.setOption(Contact.Options.DIRTY_DELETE);
3057 Account account = contact.getAccount();
3058 if (account.getStatus() == Account.State.ONLINE) {
3059 IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
3060 Element item = iq.query(Namespace.ROSTER).addChild("item");
3061 item.setAttribute("jid", contact.getJid().toString());
3062 item.setAttribute("subscription", "remove");
3063 account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
3064 }
3065 }
3066
3067 public void updateConversation(final Conversation conversation) {
3068 mDatabaseExecutor.execute(new Runnable() {
3069 @Override
3070 public void run() {
3071 databaseBackend.updateConversation(conversation);
3072 }
3073 });
3074 }
3075
3076 private void reconnectAccount(final Account account, final boolean force, final boolean interactive) {
3077 synchronized (account) {
3078 XmppConnection connection = account.getXmppConnection();
3079 if (connection == null) {
3080 connection = createConnection(account);
3081 account.setXmppConnection(connection);
3082 }
3083 boolean hasInternet = hasInternetConnection();
3084 if (!account.isOptionSet(Account.OPTION_DISABLED) && hasInternet) {
3085 if (!force) {
3086 disconnect(account, false);
3087 }
3088 Thread thread = new Thread(connection);
3089 connection.setInteractive(interactive);
3090 connection.prepareNewConnection();
3091 connection.interrupt();
3092 thread.start();
3093 scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
3094 } else {
3095 disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
3096 account.getRoster().clearPresences();
3097 connection.resetEverything();
3098 final AxolotlService axolotlService = account.getAxolotlService();
3099 if (axolotlService != null) {
3100 axolotlService.resetBrokenness();
3101 }
3102 if (!hasInternet) {
3103 account.setStatus(Account.State.NO_INTERNET);
3104 }
3105 }
3106 }
3107 }
3108
3109 public void reconnectAccountInBackground(final Account account) {
3110 new Thread(new Runnable() {
3111 @Override
3112 public void run() {
3113 reconnectAccount(account, false, true);
3114 }
3115 }).start();
3116 }
3117
3118 public void invite(Conversation conversation, Jid contact) {
3119 Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": inviting " + contact + " to " + conversation.getJid().toBareJid());
3120 MessagePacket packet = mMessageGenerator.invite(conversation, contact);
3121 sendMessagePacket(conversation.getAccount(), packet);
3122 }
3123
3124 public void directInvite(Conversation conversation, Jid jid) {
3125 MessagePacket packet = mMessageGenerator.directInvite(conversation, jid);
3126 sendMessagePacket(conversation.getAccount(), packet);
3127 }
3128
3129 public void resetSendingToWaiting(Account account) {
3130 for (Conversation conversation : getConversations()) {
3131 if (conversation.getAccount() == account) {
3132 conversation.findUnsentTextMessages(new Conversation.OnMessageFound() {
3133
3134 @Override
3135 public void onMessageFound(Message message) {
3136 markMessage(message, Message.STATUS_WAITING);
3137 }
3138 });
3139 }
3140 }
3141 }
3142
3143 public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) {
3144 return markMessage(account, recipient, uuid, status, null);
3145 }
3146
3147 public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status, String errorMessage) {
3148 if (uuid == null) {
3149 return null;
3150 }
3151 for (Conversation conversation : getConversations()) {
3152 if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) {
3153 final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
3154 if (message != null) {
3155 markMessage(message, status, errorMessage);
3156 }
3157 return message;
3158 }
3159 }
3160 return null;
3161 }
3162
3163 public boolean markMessage(Conversation conversation, String uuid, int status, String serverMessageId) {
3164 if (uuid == null) {
3165 return false;
3166 } else {
3167 Message message = conversation.findSentMessageWithUuid(uuid);
3168 if (message != null) {
3169 if (message.getServerMsgId() == null) {
3170 message.setServerMsgId(serverMessageId);
3171 }
3172 markMessage(message, status);
3173 return true;
3174 } else {
3175 return false;
3176 }
3177 }
3178 }
3179
3180 public void markMessage(Message message, int status) {
3181 markMessage(message, status, null);
3182 }
3183
3184
3185 public void markMessage(Message message, int status, String errorMessage) {
3186 if (status == Message.STATUS_SEND_FAILED
3187 && (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
3188 .getStatus() == Message.STATUS_SEND_DISPLAYED)) {
3189 return;
3190 }
3191 message.setErrorMessage(errorMessage);
3192 message.setStatus(status);
3193 databaseBackend.updateMessage(message);
3194 updateConversationUi();
3195 }
3196
3197 private SharedPreferences getPreferences() {
3198 return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
3199 }
3200
3201 public long getAutomaticMessageDeletionDate() {
3202 final long timeout = getLongPreference(SettingsActivity.AUTOMATIC_MESSAGE_DELETION,R.integer.automatic_message_deletion);
3203 return timeout == 0 ? timeout : (System.currentTimeMillis() - (timeout * 1000));
3204 }
3205
3206 public long getLongPreference(String name, @IntegerRes int res) {
3207 long defaultValue = getResources().getInteger(res);
3208 try {
3209 return Long.parseLong(getPreferences().getString(name,String.valueOf(defaultValue)));
3210 } catch (NumberFormatException e) {
3211 return defaultValue;
3212 }
3213 }
3214
3215 public boolean getBooleanPreference(String name, @BoolRes int res) {
3216 return getPreferences().getBoolean(name,getResources().getBoolean(res));
3217 }
3218
3219 public boolean confirmMessages() {
3220 return getBooleanPreference("confirm_messages", R.bool.confirm_messages);
3221 }
3222
3223 public boolean allowMessageCorrection() {
3224 return getBooleanPreference("allow_message_correction", R.bool.allow_message_correction);
3225 }
3226
3227 public boolean sendChatStates() {
3228 return getBooleanPreference("chat_states", R.bool.chat_states);
3229 }
3230
3231 private boolean respectAutojoin() {
3232 return getBooleanPreference("autojoin", R.bool.autojoin);
3233 }
3234
3235 public boolean indicateReceived() {
3236 return getBooleanPreference("indicate_received", R.bool.indicate_received);
3237 }
3238
3239 public boolean useTorToConnect() {
3240 return Config.FORCE_ORBOT || getBooleanPreference("use_tor", R.bool.use_tor);
3241 }
3242
3243 public boolean showExtendedConnectionOptions() {
3244 return getBooleanPreference("show_connection_options", R.bool.show_connection_options);
3245 }
3246
3247 public boolean broadcastLastActivity() {
3248 return getBooleanPreference(SettingsActivity.BROADCAST_LAST_ACTIVITY, R.bool.last_activity);
3249 }
3250
3251 public int unreadCount() {
3252 int count = 0;
3253 for (Conversation conversation : getConversations()) {
3254 count += conversation.unreadCount();
3255 }
3256 return count;
3257 }
3258
3259
3260 public void showErrorToastInUi(int resId) {
3261 if (mOnShowErrorToast != null) {
3262 mOnShowErrorToast.onShowErrorToast(resId);
3263 }
3264 }
3265
3266 public void updateConversationUi() {
3267 if (mOnConversationUpdate != null) {
3268 mOnConversationUpdate.onConversationUpdate();
3269 }
3270 }
3271
3272 public void updateAccountUi() {
3273 if (mOnAccountUpdate != null) {
3274 mOnAccountUpdate.onAccountUpdate();
3275 }
3276 }
3277
3278 public void updateRosterUi() {
3279 if (mOnRosterUpdate != null) {
3280 mOnRosterUpdate.onRosterUpdate();
3281 }
3282 }
3283
3284 public boolean displayCaptchaRequest(Account account, String id, Data data, Bitmap captcha) {
3285 if (mOnCaptchaRequested != null) {
3286 DisplayMetrics metrics = getApplicationContext().getResources().getDisplayMetrics();
3287 Bitmap scaled = Bitmap.createScaledBitmap(captcha, (int) (captcha.getWidth() * metrics.scaledDensity),
3288 (int) (captcha.getHeight() * metrics.scaledDensity), false);
3289
3290 mOnCaptchaRequested.onCaptchaRequested(account, id, data, scaled);
3291 return true;
3292 }
3293 return false;
3294 }
3295
3296 public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
3297 if (mOnUpdateBlocklist != null) {
3298 mOnUpdateBlocklist.OnUpdateBlocklist(status);
3299 }
3300 }
3301
3302 public void updateMucRosterUi() {
3303 if (mOnMucRosterUpdate != null) {
3304 mOnMucRosterUpdate.onMucRosterUpdate();
3305 }
3306 }
3307
3308 public void keyStatusUpdated(AxolotlService.FetchStatus report) {
3309 if (mOnKeyStatusUpdated != null) {
3310 mOnKeyStatusUpdated.onKeyStatusUpdated(report);
3311 }
3312 }
3313
3314 public Account findAccountByJid(final Jid accountJid) {
3315 for (Account account : this.accounts) {
3316 if (account.getJid().toBareJid().equals(accountJid.toBareJid())) {
3317 return account;
3318 }
3319 }
3320 return null;
3321 }
3322
3323 public Conversation findConversationByUuid(String uuid) {
3324 for (Conversation conversation : getConversations()) {
3325 if (conversation.getUuid().equals(uuid)) {
3326 return conversation;
3327 }
3328 }
3329 return null;
3330 }
3331
3332 public boolean markRead(final Conversation conversation) {
3333 return markRead(conversation,true);
3334 }
3335
3336 public boolean markRead(final Conversation conversation, boolean clear) {
3337 if (clear) {
3338 mNotificationService.clear(conversation);
3339 }
3340 final List<Message> readMessages = conversation.markRead();
3341 if (readMessages.size() > 0) {
3342 Runnable runnable = new Runnable() {
3343 @Override
3344 public void run() {
3345 for (Message message : readMessages) {
3346 databaseBackend.updateMessage(message);
3347 }
3348 }
3349 };
3350 mDatabaseExecutor.execute(runnable);
3351 updateUnreadCountBadge();
3352 return true;
3353 } else {
3354 return false;
3355 }
3356 }
3357
3358 public synchronized void updateUnreadCountBadge() {
3359 int count = unreadCount();
3360 if (unreadCount != count) {
3361 Log.d(Config.LOGTAG, "update unread count to " + count);
3362 if (count > 0) {
3363 ShortcutBadger.applyCount(getApplicationContext(), count);
3364 } else {
3365 ShortcutBadger.removeCount(getApplicationContext());
3366 }
3367 unreadCount = count;
3368 }
3369 }
3370
3371 public void sendReadMarker(final Conversation conversation) {
3372 final Message markable = conversation.getLatestMarkableMessage();
3373 if (this.markRead(conversation)) {
3374 updateConversationUi();
3375 }
3376 if (confirmMessages()
3377 && markable != null
3378 && markable.trusted()
3379 && markable.getRemoteMsgId() != null) {
3380 Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
3381 Account account = conversation.getAccount();
3382 final Jid to = markable.getCounterpart();
3383 MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId());
3384 this.sendMessagePacket(conversation.getAccount(), packet);
3385 }
3386 }
3387
3388 public SecureRandom getRNG() {
3389 return this.mRandom;
3390 }
3391
3392 public MemorizingTrustManager getMemorizingTrustManager() {
3393 return this.mMemorizingTrustManager;
3394 }
3395
3396 public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
3397 this.mMemorizingTrustManager = trustManager;
3398 }
3399
3400 public void updateMemorizingTrustmanager() {
3401 final MemorizingTrustManager tm;
3402 final boolean dontTrustSystemCAs = getBooleanPreference("dont_trust_system_cas", R.bool.dont_trust_system_cas);
3403 if (dontTrustSystemCAs) {
3404 tm = new MemorizingTrustManager(getApplicationContext(), null);
3405 } else {
3406 tm = new MemorizingTrustManager(getApplicationContext());
3407 }
3408 setMemorizingTrustManager(tm);
3409 }
3410
3411 public PowerManager getPowerManager() {
3412 return this.pm;
3413 }
3414
3415 public LruCache<String, Bitmap> getBitmapCache() {
3416 return this.mBitmapCache;
3417 }
3418
3419 public void syncRosterToDisk(final Account account) {
3420 Runnable runnable = new Runnable() {
3421
3422 @Override
3423 public void run() {
3424 databaseBackend.writeRoster(account.getRoster());
3425 }
3426 };
3427 mDatabaseExecutor.execute(runnable);
3428
3429 }
3430
3431 public List<String> getKnownHosts() {
3432 final List<String> hosts = new ArrayList<>();
3433 for (final Account account : getAccounts()) {
3434 if (!hosts.contains(account.getServer().toString())) {
3435 hosts.add(account.getServer().toString());
3436 }
3437 for (final Contact contact : account.getRoster().getContacts()) {
3438 if (contact.showInRoster()) {
3439 final String server = contact.getServer().toString();
3440 if (server != null && !hosts.contains(server)) {
3441 hosts.add(server);
3442 }
3443 }
3444 }
3445 }
3446 if(Config.DOMAIN_LOCK != null && !hosts.contains(Config.DOMAIN_LOCK)) {
3447 hosts.add(Config.DOMAIN_LOCK);
3448 }
3449 if(Config.MAGIC_CREATE_DOMAIN != null && !hosts.contains(Config.MAGIC_CREATE_DOMAIN)) {
3450 hosts.add(Config.MAGIC_CREATE_DOMAIN);
3451 }
3452 return hosts;
3453 }
3454
3455 public List<String> getKnownConferenceHosts() {
3456 final ArrayList<String> mucServers = new ArrayList<>();
3457 for (final Account account : accounts) {
3458 if (account.getXmppConnection() != null) {
3459 final String server = account.getXmppConnection().getMucServer();
3460 if (server != null && !mucServers.contains(server)) {
3461 mucServers.add(server);
3462 }
3463 for(Bookmark bookmark : account.getBookmarks()) {
3464 final Jid jid = bookmark.getJid();
3465 final String s = jid == null ? null : jid.getDomainpart();
3466 if (s != null && !mucServers.contains(s)) {
3467 mucServers.add(s);
3468 }
3469 }
3470 }
3471 }
3472 return mucServers;
3473 }
3474
3475 public void sendMessagePacket(Account account, MessagePacket packet) {
3476 XmppConnection connection = account.getXmppConnection();
3477 if (connection != null) {
3478 connection.sendMessagePacket(packet);
3479 }
3480 }
3481
3482 public void sendPresencePacket(Account account, PresencePacket packet) {
3483 XmppConnection connection = account.getXmppConnection();
3484 if (connection != null) {
3485 connection.sendPresencePacket(packet);
3486 }
3487 }
3488
3489 public void sendCreateAccountWithCaptchaPacket(Account account, String id, Data data) {
3490 final XmppConnection connection = account.getXmppConnection();
3491 if (connection != null) {
3492 IqPacket request = mIqGenerator.generateCreateAccountWithCaptcha(account, id, data);
3493 connection.sendUnmodifiedIqPacket(request, connection.registrationResponseListener);
3494 }
3495 }
3496
3497 public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
3498 final XmppConnection connection = account.getXmppConnection();
3499 if (connection != null) {
3500 connection.sendIqPacket(packet, callback);
3501 }
3502 }
3503
3504 public void sendPresence(final Account account) {
3505 sendPresence(account, checkListeners() && broadcastLastActivity());
3506 }
3507
3508 private void sendPresence(final Account account, final boolean includeIdleTimestamp) {
3509 PresencePacket packet;
3510 if (manuallyChangePresence()) {
3511 packet = mPresenceGenerator.selfPresence(account, account.getPresenceStatus());
3512 String message = account.getPresenceStatusMessage();
3513 if (message != null && !message.isEmpty()) {
3514 packet.addChild(new Element("status").setContent(message));
3515 }
3516 } else {
3517 packet = mPresenceGenerator.selfPresence(account, getTargetPresence());
3518 }
3519 if (mLastActivity > 0 && includeIdleTimestamp) {
3520 long since = Math.min(mLastActivity, System.currentTimeMillis()); //don't send future dates
3521 packet.addChild("idle",Namespace.IDLE).setAttribute("since", AbstractGenerator.getTimestamp(since));
3522 }
3523 sendPresencePacket(account, packet);
3524 }
3525
3526 private void deactivateGracePeriod() {
3527 for(Account account : getAccounts()) {
3528 account.deactivateGracePeriod();
3529 }
3530 }
3531
3532 public void refreshAllPresences() {
3533 boolean includeIdleTimestamp = checkListeners() && broadcastLastActivity();
3534 for (Account account : getAccounts()) {
3535 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
3536 sendPresence(account, includeIdleTimestamp);
3537 }
3538 }
3539 }
3540
3541 private void refreshAllGcmTokens() {
3542 for(Account account : getAccounts()) {
3543 if (account.isOnlineAndConnected() && mPushManagementService.available(account)) {
3544 mPushManagementService.registerPushTokenOnServer(account);
3545 }
3546 }
3547 }
3548
3549 private void sendOfflinePresence(final Account account) {
3550 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": sending offline presence");
3551 sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
3552 }
3553
3554 public MessageGenerator getMessageGenerator() {
3555 return this.mMessageGenerator;
3556 }
3557
3558 public PresenceGenerator getPresenceGenerator() {
3559 return this.mPresenceGenerator;
3560 }
3561
3562 public IqGenerator getIqGenerator() {
3563 return this.mIqGenerator;
3564 }
3565
3566 public IqParser getIqParser() {
3567 return this.mIqParser;
3568 }
3569
3570 public JingleConnectionManager getJingleConnectionManager() {
3571 return this.mJingleConnectionManager;
3572 }
3573
3574 public MessageArchiveService getMessageArchiveService() {
3575 return this.mMessageArchiveService;
3576 }
3577
3578 public List<Contact> findContacts(Jid jid, String accountJid) {
3579 ArrayList<Contact> contacts = new ArrayList<>();
3580 for (Account account : getAccounts()) {
3581 if ((!account.isOptionSet(Account.OPTION_DISABLED) || accountJid != null)
3582 && (accountJid == null || accountJid.equals(account.getJid().toBareJid().toString()))) {
3583 Contact contact = account.getRoster().getContactFromRoster(jid);
3584 if (contact != null) {
3585 contacts.add(contact);
3586 }
3587 }
3588 }
3589 return contacts;
3590 }
3591
3592 public Conversation findFirstMuc(Jid jid) {
3593 for(Conversation conversation : getConversations()) {
3594 if (conversation.getJid().toBareJid().equals(jid.toBareJid())
3595 && conversation.getMode() == Conversation.MODE_MULTI) {
3596 return conversation;
3597 }
3598 }
3599 return null;
3600 }
3601
3602 public NotificationService getNotificationService() {
3603 return this.mNotificationService;
3604 }
3605
3606 public HttpConnectionManager getHttpConnectionManager() {
3607 return this.mHttpConnectionManager;
3608 }
3609
3610 public void resendFailedMessages(final Message message) {
3611 final Collection<Message> messages = new ArrayList<>();
3612 Message current = message;
3613 while (current.getStatus() == Message.STATUS_SEND_FAILED) {
3614 messages.add(current);
3615 if (current.mergeable(current.next())) {
3616 current = current.next();
3617 } else {
3618 break;
3619 }
3620 }
3621 for (final Message msg : messages) {
3622 msg.setTime(System.currentTimeMillis());
3623 markMessage(msg, Message.STATUS_WAITING);
3624 this.resendMessage(msg, false);
3625 }
3626 }
3627
3628 public void clearConversationHistory(final Conversation conversation) {
3629 final long clearDate;
3630 final String reference;
3631 if (conversation.countMessages() > 0) {
3632 Message latestMessage = conversation.getLatestMessage();
3633 clearDate = latestMessage.getTimeSent() + 1000;
3634 reference = latestMessage.getServerMsgId();
3635 } else {
3636 clearDate = System.currentTimeMillis();
3637 reference = null;
3638 }
3639 conversation.clearMessages();
3640 conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam
3641 conversation.setLastClearHistory(clearDate,reference);
3642 Runnable runnable = new Runnable() {
3643 @Override
3644 public void run() {
3645 databaseBackend.deleteMessagesInConversation(conversation);
3646 databaseBackend.updateConversation(conversation);
3647 }
3648 };
3649 mDatabaseExecutor.execute(runnable);
3650 }
3651
3652 public boolean sendBlockRequest(final Blockable blockable, boolean reportSpam) {
3653 if (blockable != null && blockable.getBlockedJid() != null) {
3654 final Jid jid = blockable.getBlockedJid();
3655 this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid, reportSpam), new OnIqPacketReceived() {
3656
3657 @Override
3658 public void onIqPacketReceived(final Account account, final IqPacket packet) {
3659 if (packet.getType() == IqPacket.TYPE.RESULT) {
3660 account.getBlocklist().add(jid);
3661 updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
3662 }
3663 }
3664 });
3665 if (removeBlockedConversations(blockable.getAccount(),jid)) {
3666 updateConversationUi();
3667 return true;
3668 } else {
3669 return false;
3670 }
3671 } else {
3672 return false;
3673 }
3674 }
3675
3676 public boolean removeBlockedConversations(final Account account, final Jid blockedJid) {
3677 boolean removed = false;
3678 synchronized (this.conversations) {
3679 boolean domainJid = blockedJid.isDomainJid();
3680 for(Conversation conversation : this.conversations) {
3681 boolean jidMatches = (domainJid && blockedJid.getDomainpart().equals(conversation.getJid().getDomainpart()))
3682 || blockedJid.equals(conversation.getJid().toBareJid());
3683 if (conversation.getAccount() == account
3684 && conversation.getMode() == Conversation.MODE_SINGLE
3685 && jidMatches) {
3686 this.conversations.remove(conversation);
3687 markRead(conversation);
3688 conversation.setStatus(Conversation.STATUS_ARCHIVED);
3689 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": archiving conversation "+conversation.getJid().toBareJid()+" because jid was blocked");
3690 updateConversation(conversation);
3691 removed = true;
3692 }
3693 }
3694 }
3695 return removed;
3696 }
3697
3698 public void sendUnblockRequest(final Blockable blockable) {
3699 if (blockable != null && blockable.getJid() != null) {
3700 final Jid jid = blockable.getBlockedJid();
3701 this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() {
3702 @Override
3703 public void onIqPacketReceived(final Account account, final IqPacket packet) {
3704 if (packet.getType() == IqPacket.TYPE.RESULT) {
3705 account.getBlocklist().remove(jid);
3706 updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
3707 }
3708 }
3709 });
3710 }
3711 }
3712
3713 public void publishDisplayName(Account account) {
3714 String displayName = account.getDisplayName();
3715 if (displayName != null && !displayName.isEmpty()) {
3716 IqPacket publish = mIqGenerator.publishNick(displayName);
3717 sendIqPacket(account, publish, new OnIqPacketReceived() {
3718 @Override
3719 public void onIqPacketReceived(Account account, IqPacket packet) {
3720 if (packet.getType() == IqPacket.TYPE.ERROR) {
3721 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not publish nick");
3722 }
3723 }
3724 });
3725 }
3726 }
3727
3728 public ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String, String> key) {
3729 ServiceDiscoveryResult result = discoCache.get(key);
3730 if (result != null) {
3731 return result;
3732 } else {
3733 result = databaseBackend.findDiscoveryResult(key.first, key.second);
3734 if (result != null) {
3735 discoCache.put(key, result);
3736 }
3737 return result;
3738 }
3739 }
3740
3741 public void fetchCaps(Account account, final Jid jid, final Presence presence) {
3742 final Pair<String,String> key = new Pair<>(presence.getHash(), presence.getVer());
3743 ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key);
3744 if (disco != null) {
3745 presence.setServiceDiscoveryResult(disco);
3746 } else {
3747 if (!account.inProgressDiscoFetches.contains(key)) {
3748 account.inProgressDiscoFetches.add(key);
3749 IqPacket request = new IqPacket(IqPacket.TYPE.GET);
3750 request.setTo(jid);
3751 request.query("http://jabber.org/protocol/disco#info");
3752 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": making disco request for "+key.second+" to "+jid);
3753 sendIqPacket(account, request, new OnIqPacketReceived() {
3754 @Override
3755 public void onIqPacketReceived(Account account, IqPacket discoPacket) {
3756 if (discoPacket.getType() == IqPacket.TYPE.RESULT) {
3757 ServiceDiscoveryResult disco = new ServiceDiscoveryResult(discoPacket);
3758 if (presence.getVer().equals(disco.getVer())) {
3759 databaseBackend.insertDiscoveryResult(disco);
3760 injectServiceDiscorveryResult(account.getRoster(), presence.getHash(), presence.getVer(), disco);
3761 } else {
3762 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + disco.getVer());
3763 }
3764 }
3765 account.inProgressDiscoFetches.remove(key);
3766 }
3767 });
3768 }
3769 }
3770 }
3771
3772 private void injectServiceDiscorveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) {
3773 for(Contact contact : roster.getContacts()) {
3774 for(Presence presence : contact.getPresences().getPresences().values()) {
3775 if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) {
3776 presence.setServiceDiscoveryResult(disco);
3777 }
3778 }
3779 }
3780 }
3781
3782 public void fetchMamPreferences(Account account, final OnMamPreferencesFetched callback) {
3783 final boolean legacy = account.getXmppConnection().getFeatures().mamLegacy();
3784 IqPacket request = new IqPacket(IqPacket.TYPE.GET);
3785 request.addChild("prefs",legacy ? Namespace.MAM_LEGACY : Namespace.MAM);
3786 sendIqPacket(account, request, new OnIqPacketReceived() {
3787 @Override
3788 public void onIqPacketReceived(Account account, IqPacket packet) {
3789 Element prefs = packet.findChild("prefs",legacy ? Namespace.MAM_LEGACY : Namespace.MAM);
3790 if (packet.getType() == IqPacket.TYPE.RESULT && prefs != null) {
3791 callback.onPreferencesFetched(prefs);
3792 } else {
3793 callback.onPreferencesFetchFailed();
3794 }
3795 }
3796 });
3797 }
3798
3799 public PushManagementService getPushManagementService() {
3800 return mPushManagementService;
3801 }
3802
3803 public Account getPendingAccount() {
3804 Account pending = null;
3805 for(Account account : getAccounts()) {
3806 if (account.isOptionSet(Account.OPTION_REGISTER)) {
3807 pending = account;
3808 } else {
3809 return null;
3810 }
3811 }
3812 return pending;
3813 }
3814
3815 public void changeStatus(Account account, Presence.Status status, String statusMessage, boolean send) {
3816 if (!statusMessage.isEmpty()) {
3817 databaseBackend.insertPresenceTemplate(new PresenceTemplate(status, statusMessage));
3818 }
3819 changeStatusReal(account, status, statusMessage, send);
3820 }
3821
3822 private void changeStatusReal(Account account, Presence.Status status, String statusMessage, boolean send) {
3823 account.setPresenceStatus(status);
3824 account.setPresenceStatusMessage(statusMessage);
3825 databaseBackend.updateAccount(account);
3826 if (!account.isOptionSet(Account.OPTION_DISABLED) && send) {
3827 sendPresence(account);
3828 }
3829 }
3830
3831 public void changeStatus(Presence.Status status, String statusMessage) {
3832 if (!statusMessage.isEmpty()) {
3833 databaseBackend.insertPresenceTemplate(new PresenceTemplate(status, statusMessage));
3834 }
3835 for(Account account : getAccounts()) {
3836 changeStatusReal(account, status, statusMessage, true);
3837 }
3838 }
3839
3840 public List<PresenceTemplate> getPresenceTemplates(Account account) {
3841 List<PresenceTemplate> templates = databaseBackend.getPresenceTemplates();
3842 for(PresenceTemplate template : account.getSelfContact().getPresences().asTemplates()) {
3843 if (!templates.contains(template)) {
3844 templates.add(0, template);
3845 }
3846 }
3847 return templates;
3848 }
3849
3850 public void saveConversationAsBookmark(Conversation conversation, String name) {
3851 Account account = conversation.getAccount();
3852 Bookmark bookmark = new Bookmark(account, conversation.getJid().toBareJid());
3853 if (!conversation.getJid().isBareJid()) {
3854 bookmark.setNick(conversation.getJid().getResourcepart());
3855 }
3856 if (name != null && !name.trim().isEmpty()) {
3857 bookmark.setBookmarkName(name.trim());
3858 }
3859 bookmark.setAutojoin(getPreferences().getBoolean("autojoin",getResources().getBoolean(R.bool.autojoin)));
3860 account.getBookmarks().add(bookmark);
3861 pushBookmarks(account);
3862 conversation.setBookmark(bookmark);
3863 }
3864
3865 public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
3866 boolean needsRosterWrite = false;
3867 boolean performedVerification = false;
3868 final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
3869 for(XmppUri.Fingerprint fp : fingerprints) {
3870 if (fp.type == XmppUri.FingerprintType.OTR) {
3871 performedVerification |= contact.addOtrFingerprint(fp.fingerprint);
3872 needsRosterWrite |= performedVerification;
3873 } else if (fp.type == XmppUri.FingerprintType.OMEMO) {
3874 String fingerprint = "05"+fp.fingerprint.replaceAll("\\s","");
3875 FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
3876 if (fingerprintStatus != null) {
3877 if (!fingerprintStatus.isVerified()) {
3878 performedVerification = true;
3879 axolotlService.setFingerprintTrust(fingerprint,fingerprintStatus.toVerified());
3880 }
3881 } else {
3882 axolotlService.preVerifyFingerprint(contact,fingerprint);
3883 }
3884 }
3885 }
3886 if (needsRosterWrite) {
3887 syncRosterToDisk(contact.getAccount());
3888 }
3889 return performedVerification;
3890 }
3891
3892 public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
3893 final AxolotlService axolotlService = account.getAxolotlService();
3894 boolean verifiedSomething = false;
3895 for(XmppUri.Fingerprint fp : fingerprints) {
3896 if (fp.type == XmppUri.FingerprintType.OMEMO) {
3897 String fingerprint = "05"+fp.fingerprint.replaceAll("\\s","");
3898 Log.d(Config.LOGTAG,"trying to verify own fp="+fingerprint);
3899 FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
3900 if (fingerprintStatus != null) {
3901 if (!fingerprintStatus.isVerified()) {
3902 axolotlService.setFingerprintTrust(fingerprint,fingerprintStatus.toVerified());
3903 verifiedSomething = true;
3904 }
3905 } else {
3906 axolotlService.preVerifyFingerprint(account,fingerprint);
3907 verifiedSomething = true;
3908 }
3909 }
3910 }
3911 return verifiedSomething;
3912 }
3913
3914 public boolean blindTrustBeforeVerification() {
3915 return getBooleanPreference(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION,R.bool.btbv);
3916 }
3917
3918 public ShortcutService getShortcutService() {
3919 return mShortcutService;
3920 }
3921
3922 public interface OnMamPreferencesFetched {
3923 void onPreferencesFetched(Element prefs);
3924 void onPreferencesFetchFailed();
3925 }
3926
3927 public void pushMamPreferences(Account account, Element prefs) {
3928 IqPacket set = new IqPacket(IqPacket.TYPE.SET);
3929 set.addChild(prefs);
3930 sendIqPacket(account, set, null);
3931 }
3932
3933 public interface OnAccountCreated {
3934 void onAccountCreated(Account account);
3935
3936 void informUser(int r);
3937 }
3938
3939 public interface OnMoreMessagesLoaded {
3940 void onMoreMessagesLoaded(int count, Conversation conversation);
3941
3942 void informUser(int r);
3943 }
3944
3945 public interface OnAccountPasswordChanged {
3946 void onPasswordChangeSucceeded();
3947
3948 void onPasswordChangeFailed();
3949 }
3950
3951 public interface OnAffiliationChanged {
3952 void onAffiliationChangedSuccessful(Jid jid);
3953
3954 void onAffiliationChangeFailed(Jid jid, int resId);
3955 }
3956
3957 public interface OnRoleChanged {
3958 void onRoleChangedSuccessful(String nick);
3959
3960 void onRoleChangeFailed(String nick, int resid);
3961 }
3962
3963 public interface OnConversationUpdate {
3964 void onConversationUpdate();
3965 }
3966
3967 public interface OnAccountUpdate {
3968 void onAccountUpdate();
3969 }
3970
3971 public interface OnCaptchaRequested {
3972 void onCaptchaRequested(Account account,
3973 String id,
3974 Data data,
3975 Bitmap captcha);
3976 }
3977
3978 public interface OnRosterUpdate {
3979 void onRosterUpdate();
3980 }
3981
3982 public interface OnMucRosterUpdate {
3983 void onMucRosterUpdate();
3984 }
3985
3986 public interface OnConferenceConfigurationFetched {
3987 void onConferenceConfigurationFetched(Conversation conversation);
3988
3989 void onFetchFailed(Conversation conversation, Element error);
3990 }
3991
3992 public interface OnConferenceJoined {
3993 void onConferenceJoined(Conversation conversation);
3994 }
3995
3996 public interface OnConfigurationPushed {
3997 void onPushSucceeded();
3998
3999 void onPushFailed();
4000 }
4001
4002 public interface OnShowErrorToast {
4003 void onShowErrorToast(int resId);
4004 }
4005
4006 public class XmppConnectionBinder extends Binder {
4007 public XmppConnectionService getService() {
4008 return XmppConnectionService.this;
4009 }
4010 }
4011}