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