1package eu.siacs.conversations.services;
2
3import android.annotation.SuppressLint;
4import android.app.AlarmManager;
5import android.app.PendingIntent;
6import android.app.Service;
7import android.content.Context;
8import android.content.Intent;
9import android.content.IntentFilter;
10import android.content.SharedPreferences;
11import android.database.ContentObserver;
12import android.graphics.Bitmap;
13import android.media.AudioManager;
14import android.net.ConnectivityManager;
15import android.net.NetworkInfo;
16import android.net.Uri;
17import android.os.Binder;
18import android.os.Build;
19import android.os.Bundle;
20import android.os.FileObserver;
21import android.os.IBinder;
22import android.os.Looper;
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.security.KeyChainException;
30import android.util.Log;
31import android.util.LruCache;
32
33import net.java.otr4j.OtrException;
34import net.java.otr4j.session.Session;
35import net.java.otr4j.session.SessionID;
36import net.java.otr4j.session.SessionImpl;
37import net.java.otr4j.session.SessionStatus;
38
39import org.bouncycastle.asn1.x500.RDN;
40import org.bouncycastle.asn1.x500.X500Name;
41import org.bouncycastle.asn1.x500.style.BCStyle;
42import org.bouncycastle.asn1.x500.style.IETFUtils;
43import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder;
44import org.bouncycastle.jce.PrincipalUtil;
45import org.bouncycastle.jce.X509Principal;
46import org.openintents.openpgp.util.OpenPgpApi;
47import org.openintents.openpgp.util.OpenPgpServiceConnection;
48
49import java.math.BigInteger;
50import java.security.PrivateKey;
51import java.security.SecureRandom;
52import java.security.cert.CertificateEncodingException;
53import java.security.cert.CertificateParsingException;
54import java.security.cert.X509Certificate;
55import java.util.ArrayList;
56import java.util.Arrays;
57import java.util.Collection;
58import java.util.Collections;
59import java.util.Comparator;
60import java.util.Hashtable;
61import java.util.Iterator;
62import java.util.List;
63import java.util.Locale;
64import java.util.Map;
65import java.util.concurrent.CopyOnWriteArrayList;
66
67import de.duenndns.ssl.MemorizingTrustManager;
68import eu.siacs.conversations.Config;
69import eu.siacs.conversations.R;
70import eu.siacs.conversations.crypto.PgpEngine;
71import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
72import eu.siacs.conversations.entities.Account;
73import eu.siacs.conversations.entities.Blockable;
74import eu.siacs.conversations.entities.Bookmark;
75import eu.siacs.conversations.entities.Contact;
76import eu.siacs.conversations.entities.Conversation;
77import eu.siacs.conversations.entities.Message;
78import eu.siacs.conversations.entities.MucOptions;
79import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
80import eu.siacs.conversations.entities.Presences;
81import eu.siacs.conversations.entities.Transferable;
82import eu.siacs.conversations.entities.TransferablePlaceholder;
83import eu.siacs.conversations.generator.IqGenerator;
84import eu.siacs.conversations.generator.MessageGenerator;
85import eu.siacs.conversations.generator.PresenceGenerator;
86import eu.siacs.conversations.http.HttpConnectionManager;
87import eu.siacs.conversations.parser.IqParser;
88import eu.siacs.conversations.parser.MessageParser;
89import eu.siacs.conversations.parser.PresenceParser;
90import eu.siacs.conversations.persistance.DatabaseBackend;
91import eu.siacs.conversations.persistance.FileBackend;
92import eu.siacs.conversations.ui.UiCallback;
93import eu.siacs.conversations.utils.CryptoHelper;
94import eu.siacs.conversations.utils.ExceptionHelper;
95import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
96import eu.siacs.conversations.utils.PRNGFixes;
97import eu.siacs.conversations.utils.PhoneHelper;
98import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
99import eu.siacs.conversations.utils.Xmlns;
100import eu.siacs.conversations.xml.Element;
101import eu.siacs.conversations.xmpp.OnBindListener;
102import eu.siacs.conversations.xmpp.OnContactStatusChanged;
103import eu.siacs.conversations.xmpp.OnIqPacketReceived;
104import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
105import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
106import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
107import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
108import eu.siacs.conversations.xmpp.OnStatusChanged;
109import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
110import eu.siacs.conversations.xmpp.XmppConnection;
111import eu.siacs.conversations.xmpp.chatstate.ChatState;
112import eu.siacs.conversations.xmpp.forms.Data;
113import eu.siacs.conversations.xmpp.forms.Field;
114import eu.siacs.conversations.xmpp.jid.InvalidJidException;
115import eu.siacs.conversations.xmpp.jid.Jid;
116import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
117import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
118import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
119import eu.siacs.conversations.xmpp.pep.Avatar;
120import eu.siacs.conversations.xmpp.stanzas.IqPacket;
121import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
122import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
123import me.leolin.shortcutbadger.ShortcutBadger;
124
125public class XmppConnectionService extends Service implements OnPhoneContactsLoadedListener {
126
127 public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification";
128 public static final String ACTION_DISABLE_FOREGROUND = "disable_foreground";
129 private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
130 public static final String ACTION_TRY_AGAIN = "try_again";
131 public static final String ACTION_DISABLE_ACCOUNT = "disable_account";
132 private ContentObserver contactObserver = new ContentObserver(null) {
133 @Override
134 public void onChange(boolean selfChange) {
135 super.onChange(selfChange);
136 Intent intent = new Intent(getApplicationContext(),
137 XmppConnectionService.class);
138 intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
139 startService(intent);
140 }
141 };
142
143 private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor();
144 private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor();
145
146 private final IBinder mBinder = new XmppConnectionBinder();
147 private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
148 private final FileObserver fileObserver = new FileObserver(
149 FileBackend.getConversationsImageDirectory()) {
150
151 @Override
152 public void onEvent(int event, String path) {
153 if (event == FileObserver.DELETE) {
154 markFileDeleted(path.split("\\.")[0]);
155 }
156 }
157 };
158 private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
159
160 @Override
161 public void onJinglePacketReceived(Account account, JinglePacket packet) {
162 mJingleConnectionManager.deliverPacket(account, packet);
163 }
164 };
165 private final OnBindListener mOnBindListener = new OnBindListener() {
166
167 @Override
168 public void onBind(final Account account) {
169 account.getRoster().clearPresences();
170 fetchRosterFromServer(account);
171 fetchBookmarks(account);
172 sendPresence(account);
173 connectMultiModeConversations(account);
174 mMessageArchiveService.executePendingQueries(account);
175 mJingleConnectionManager.cancelInTransmission();
176 syncDirtyContacts(account);
177 account.getAxolotlService().publishBundlesIfNeeded(true);
178 }
179 };
180 private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
181
182 @Override
183 public void onMessageAcknowledged(Account account, String uuid) {
184 for (final Conversation conversation : getConversations()) {
185 if (conversation.getAccount() == account) {
186 Message message = conversation.findUnsentMessageWithUuid(uuid);
187 if (message != null) {
188 markMessage(message, Message.STATUS_SEND);
189 if (conversation.setLastMessageTransmitted(System.currentTimeMillis())) {
190 databaseBackend.updateConversation(conversation);
191 }
192 }
193 }
194 }
195 }
196 };
197 private final IqGenerator mIqGenerator = new IqGenerator(this);
198 public DatabaseBackend databaseBackend;
199 public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
200
201 @Override
202 public void onContactStatusChanged(Contact contact, boolean online) {
203 Conversation conversation = find(getConversations(), contact);
204 if (conversation != null) {
205 if (online) {
206 conversation.endOtrIfNeeded();
207 if (contact.getPresences().size() == 1) {
208 sendUnsentMessages(conversation);
209 }
210 } else {
211 if (contact.getPresences().size() >= 1) {
212 if (conversation.hasValidOtrSession()) {
213 String otrResource = conversation.getOtrSession().getSessionID().getUserID();
214 if (!(Arrays.asList(contact.getPresences().asStringArray()).contains(otrResource))) {
215 conversation.endOtrIfNeeded();
216 }
217 }
218 } else {
219 conversation.endOtrIfNeeded();
220 }
221 }
222 }
223 }
224 };
225 private FileBackend fileBackend = new FileBackend(this);
226 private MemorizingTrustManager mMemorizingTrustManager;
227 private NotificationService mNotificationService = new NotificationService(
228 this);
229 private OnMessagePacketReceived mMessageParser = new MessageParser(this);
230 private OnPresencePacketReceived mPresenceParser = new PresenceParser(this);
231 private IqParser mIqParser = new IqParser(this);
232 private OnIqPacketReceived mDefaultIqHandler = new OnIqPacketReceived() {
233 @Override
234 public void onIqPacketReceived(Account account, IqPacket packet) {
235 if (packet.getType() != IqPacket.TYPE.RESULT) {
236 Element error = packet.findChild("error");
237 String text = error != null ? error.findChildContent("text") : null;
238 if (text != null) {
239 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received iq error - "+text);
240 }
241 }
242 }
243 };
244 private MessageGenerator mMessageGenerator = new MessageGenerator(this);
245 private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
246 private List<Account> accounts;
247 private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
248 this);
249 private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
250 this);
251 private AvatarService mAvatarService = new AvatarService(this);
252 private final List<String> mInProgressAvatarFetches = new ArrayList<>();
253 private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
254 private OnConversationUpdate mOnConversationUpdate = null;
255 private int convChangedListenerCount = 0;
256 private OnShowErrorToast mOnShowErrorToast = null;
257 private int showErrorToastListenerCount = 0;
258 private int unreadCount = -1;
259 private OnAccountUpdate mOnAccountUpdate = null;
260 private OnStatusChanged statusListener = new OnStatusChanged() {
261
262 @Override
263 public void onStatusChanged(Account account) {
264 XmppConnection connection = account.getXmppConnection();
265 if (mOnAccountUpdate != null) {
266 mOnAccountUpdate.onAccountUpdate();
267 }
268 if (account.getStatus() == Account.State.ONLINE) {
269 if (connection != null && connection.getFeatures().csi()) {
270 if (checkListeners()) {
271 Log.d(Config.LOGTAG, account.getJid().toBareJid()+ " sending csi//inactive");
272 connection.sendInactive();
273 } else {
274 Log.d(Config.LOGTAG, account.getJid().toBareJid()+ " sending csi//active");
275 connection.sendActive();
276 }
277 }
278 List<Conversation> conversations = getConversations();
279 for (Conversation conversation : conversations) {
280 if (conversation.getAccount() == account) {
281 conversation.startOtrIfNeeded();
282 sendUnsentMessages(conversation);
283 }
284 }
285 for (Conversation conversation : account.pendingConferenceLeaves) {
286 leaveMuc(conversation);
287 }
288 account.pendingConferenceLeaves.clear();
289 for (Conversation conversation : account.pendingConferenceJoins) {
290 joinMuc(conversation);
291 }
292 account.pendingConferenceJoins.clear();
293 scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
294 } else if (account.getStatus() == Account.State.OFFLINE) {
295 resetSendingToWaiting(account);
296 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
297 int timeToReconnect = mRandom.nextInt(20) + 10;
298 scheduleWakeUpCall(timeToReconnect,account.getUuid().hashCode());
299 }
300 } else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
301 databaseBackend.updateAccount(account);
302 reconnectAccount(account, true, false);
303 } else if ((account.getStatus() != Account.State.CONNECTING)
304 && (account.getStatus() != Account.State.NO_INTERNET)) {
305 if (connection != null) {
306 int next = connection.getTimeToNextAttempt();
307 Log.d(Config.LOGTAG, account.getJid().toBareJid()
308 + ": error connecting account. try again in "
309 + next + "s for the "
310 + (connection.getAttempt() + 1) + " time");
311 scheduleWakeUpCall(next,account.getUuid().hashCode());
312 }
313 }
314 getNotificationService().updateErrorNotification();
315 }
316 };
317 private int accountChangedListenerCount = 0;
318 private OnRosterUpdate mOnRosterUpdate = null;
319 private OnUpdateBlocklist mOnUpdateBlocklist = null;
320 private int updateBlocklistListenerCount = 0;
321 private int rosterChangedListenerCount = 0;
322 private OnMucRosterUpdate mOnMucRosterUpdate = null;
323 private int mucRosterChangedListenerCount = 0;
324 private OnKeyStatusUpdated mOnKeyStatusUpdated = null;
325 private int keyStatusUpdatedListenerCount = 0;
326 private SecureRandom mRandom;
327 private OpenPgpServiceConnection pgpServiceConnection;
328 private PgpEngine mPgpEngine = null;
329 private WakeLock wakeLock;
330 private PowerManager pm;
331 private LruCache<String, Bitmap> mBitmapCache;
332 private Thread mPhoneContactMergerThread;
333 private EventReceiver mEventReceiver = new EventReceiver();
334
335 private boolean mRestoredFromDatabase = false;
336 public boolean areMessagesInitialized() {
337 return this.mRestoredFromDatabase;
338 }
339
340 public PgpEngine getPgpEngine() {
341 if (pgpServiceConnection.isBound()) {
342 if (this.mPgpEngine == null) {
343 this.mPgpEngine = new PgpEngine(new OpenPgpApi(
344 getApplicationContext(),
345 pgpServiceConnection.getService()), this);
346 }
347 return mPgpEngine;
348 } else {
349 return null;
350 }
351
352 }
353
354 public FileBackend getFileBackend() {
355 return this.fileBackend;
356 }
357
358 public AvatarService getAvatarService() {
359 return this.mAvatarService;
360 }
361
362 public void attachLocationToConversation(final Conversation conversation,
363 final Uri uri,
364 final UiCallback<Message> callback) {
365 int encryption = conversation.getNextEncryption();
366 if (encryption == Message.ENCRYPTION_PGP) {
367 encryption = Message.ENCRYPTION_DECRYPTED;
368 }
369 Message message = new Message(conversation,uri.toString(),encryption);
370 if (conversation.getNextCounterpart() != null) {
371 message.setCounterpart(conversation.getNextCounterpart());
372 }
373 if (encryption == Message.ENCRYPTION_DECRYPTED) {
374 getPgpEngine().encrypt(message, callback);
375 } else {
376 callback.success(message);
377 }
378 }
379
380 public void attachFileToConversation(final Conversation conversation,
381 final Uri uri,
382 final UiCallback<Message> callback) {
383 final Message message;
384 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
385 message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
386 } else {
387 message = new Message(conversation, "", conversation.getNextEncryption());
388 }
389 message.setCounterpart(conversation.getNextCounterpart());
390 message.setType(Message.TYPE_FILE);
391 String path = getFileBackend().getOriginalPath(uri);
392 if (path!=null) {
393 message.setRelativeFilePath(path);
394 getFileBackend().updateFileParams(message);
395 if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
396 getPgpEngine().encrypt(message, callback);
397 } else {
398 callback.success(message);
399 }
400 } else {
401 mFileAddingExecutor.execute(new Runnable() {
402 @Override
403 public void run() {
404 try {
405 getFileBackend().copyFileToPrivateStorage(message, uri);
406 getFileBackend().updateFileParams(message);
407 if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
408 getPgpEngine().encrypt(message, callback);
409 } else {
410 callback.success(message);
411 }
412 } catch (FileBackend.FileCopyException e) {
413 callback.error(e.getResId(), message);
414 }
415 }
416 });
417 }
418 }
419
420 public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
421 if (getFileBackend().useImageAsIs(uri)) {
422 Log.d(Config.LOGTAG,"using image as is");
423 attachFileToConversation(conversation, uri, callback);
424 return;
425 }
426 final Message message;
427 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
428 message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
429 } else {
430 message = new Message(conversation, "",conversation.getNextEncryption());
431 }
432 message.setCounterpart(conversation.getNextCounterpart());
433 message.setType(Message.TYPE_IMAGE);
434 mFileAddingExecutor.execute(new Runnable() {
435
436 @Override
437 public void run() {
438 try {
439 getFileBackend().copyImageToPrivateStorage(message, uri);
440 if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
441 getPgpEngine().encrypt(message, callback);
442 } else {
443 callback.success(message);
444 }
445 } catch (final FileBackend.FileCopyException e) {
446 callback.error(e.getResId(), message);
447 }
448 }
449 });
450 }
451
452 public Conversation find(Bookmark bookmark) {
453 return find(bookmark.getAccount(), bookmark.getJid());
454 }
455
456 public Conversation find(final Account account, final Jid jid) {
457 return find(getConversations(), account, jid);
458 }
459
460 @Override
461 public int onStartCommand(Intent intent, int flags, int startId) {
462 final String action = intent == null ? null : intent.getAction();
463 boolean interactive = false;
464 if (action != null) {
465 Log.d(Config.LOGTAG,"action: "+action);
466 switch (action) {
467 case ConnectivityManager.CONNECTIVITY_ACTION:
468 if (hasInternetConnection() && Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
469 resetAllAttemptCounts(true);
470 }
471 break;
472 case ACTION_MERGE_PHONE_CONTACTS:
473 if (mRestoredFromDatabase) {
474 PhoneHelper.loadPhoneContacts(getApplicationContext(),
475 new CopyOnWriteArrayList<Bundle>(),
476 this);
477 }
478 return START_STICKY;
479 case Intent.ACTION_SHUTDOWN:
480 logoutAndSave();
481 return START_NOT_STICKY;
482 case ACTION_CLEAR_NOTIFICATION:
483 mNotificationService.clear();
484 break;
485 case ACTION_DISABLE_FOREGROUND:
486 getPreferences().edit().putBoolean("keep_foreground_service",false).commit();
487 toggleForegroundService();
488 break;
489 case ACTION_TRY_AGAIN:
490 resetAllAttemptCounts(false);
491 interactive = true;
492 break;
493 case ACTION_DISABLE_ACCOUNT:
494 try {
495 String jid = intent.getStringExtra("account");
496 Account account = jid == null ? null : findAccountByJid(Jid.fromString(jid));
497 if (account != null) {
498 account.setOption(Account.OPTION_DISABLED,true);
499 updateAccount(account);
500 }
501 } catch (final InvalidJidException ignored) {
502 break;
503 }
504 break;
505 case AudioManager.RINGER_MODE_CHANGED_ACTION:
506 if (xaOnSilentMode()) {
507 refreshAllPresences();
508 }
509 break;
510 case Intent.ACTION_SCREEN_OFF:
511 case Intent.ACTION_SCREEN_ON:
512 if (awayWhenScreenOff()) {
513 refreshAllPresences();
514 }
515 break;
516 }
517 }
518 this.wakeLock.acquire();
519
520 for (Account account : accounts) {
521 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
522 if (!hasInternetConnection()) {
523 account.setStatus(Account.State.NO_INTERNET);
524 if (statusListener != null) {
525 statusListener.onStatusChanged(account);
526 }
527 } else {
528 if (account.getStatus() == Account.State.NO_INTERNET) {
529 account.setStatus(Account.State.OFFLINE);
530 if (statusListener != null) {
531 statusListener.onStatusChanged(account);
532 }
533 }
534 if (account.getStatus() == Account.State.ONLINE) {
535 long lastReceived = account.getXmppConnection().getLastPacketReceived();
536 long lastSent = account.getXmppConnection().getLastPingSent();
537 long pingInterval = "ui".equals(action) ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
538 long msToNextPing = (Math.max(lastReceived,lastSent) + pingInterval) - SystemClock.elapsedRealtime();
539 long pingTimeoutIn = (lastSent + Config.PING_TIMEOUT * 1000) - SystemClock.elapsedRealtime();
540 if (lastSent > lastReceived) {
541 if (pingTimeoutIn < 0) {
542 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ping timeout");
543 this.reconnectAccount(account, true, interactive);
544 } else {
545 int secs = (int) (pingTimeoutIn / 1000);
546 this.scheduleWakeUpCall(secs,account.getUuid().hashCode());
547 }
548 } else if (msToNextPing <= 0) {
549 account.getXmppConnection().sendPing();
550 Log.d(Config.LOGTAG, account.getJid().toBareJid()+" send ping");
551 this.scheduleWakeUpCall(Config.PING_TIMEOUT,account.getUuid().hashCode());
552 } else {
553 this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
554 }
555 } else if (account.getStatus() == Account.State.OFFLINE) {
556 reconnectAccount(account,true, interactive);
557 } else if (account.getStatus() == Account.State.CONNECTING) {
558 long timeout = Config.CONNECT_TIMEOUT - ((SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000);
559 if (timeout < 0) {
560 Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting");
561 reconnectAccount(account, true, interactive);
562 } else {
563 scheduleWakeUpCall((int) timeout,account.getUuid().hashCode());
564 }
565 } else {
566 if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
567 reconnectAccount(account, true, interactive);
568 }
569 }
570
571 }
572 if (mOnAccountUpdate != null) {
573 mOnAccountUpdate.onAccountUpdate();
574 }
575 }
576 }
577 if (wakeLock.isHeld()) {
578 try {
579 wakeLock.release();
580 } catch (final RuntimeException ignored) {
581 }
582 }
583 return START_STICKY;
584 }
585
586 private boolean xaOnSilentMode() {
587 return getPreferences().getBoolean("xa_on_silent_mode", false);
588 }
589
590 private boolean awayWhenScreenOff() {
591 return getPreferences().getBoolean("away_when_screen_off", false);
592 }
593
594 private int getTargetPresence() {
595 if (xaOnSilentMode() && isPhoneSilenced()) {
596 return Presences.XA;
597 } else if (awayWhenScreenOff() && !isInteractive()) {
598 return Presences.AWAY;
599 } else {
600 return Presences.ONLINE;
601 }
602 }
603
604 @SuppressLint("NewApi")
605 @SuppressWarnings("deprecation")
606 public boolean isInteractive() {
607 final PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
608
609 final boolean isScreenOn;
610 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
611 isScreenOn = pm.isScreenOn();
612 } else {
613 isScreenOn = pm.isInteractive();
614 }
615 return isScreenOn;
616 }
617
618 private boolean isPhoneSilenced() {
619 AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
620 return audioManager.getRingerMode() == AudioManager.RINGER_MODE_SILENT;
621 }
622
623 private void resetAllAttemptCounts(boolean reallyAll) {
624 Log.d(Config.LOGTAG,"resetting all attepmt counts");
625 for(Account account : accounts) {
626 if (account.hasErrorStatus() || reallyAll) {
627 final XmppConnection connection = account.getXmppConnection();
628 if (connection != null) {
629 connection.resetAttemptCount();
630 }
631 }
632 }
633 }
634
635 public boolean hasInternetConnection() {
636 ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
637 .getSystemService(Context.CONNECTIVITY_SERVICE);
638 NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
639 return activeNetwork != null && activeNetwork.isConnected();
640 }
641
642 @SuppressLint("TrulyRandom")
643 @Override
644 public void onCreate() {
645 ExceptionHelper.init(getApplicationContext());
646 PRNGFixes.apply();
647 this.mRandom = new SecureRandom();
648 updateMemorizingTrustmanager();
649 final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
650 final int cacheSize = maxMemory / 8;
651 this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
652 @Override
653 protected int sizeOf(final String key, final Bitmap bitmap) {
654 return bitmap.getByteCount() / 1024;
655 }
656 };
657
658 this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
659 this.accounts = databaseBackend.getAccounts();
660
661 restoreFromDatabase();
662
663 getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
664 this.fileObserver.startWatching();
665
666 this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain");
667 this.pgpServiceConnection.bindToService();
668
669 this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
670 this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"XmppConnectionService");
671 toggleForegroundService();
672 updateUnreadCountBadge();
673 toggleScreenEventReceiver();
674 }
675
676 @Override
677 public void onDestroy() {
678 try {
679 unregisterReceiver(this.mEventReceiver);
680 } catch (IllegalArgumentException e) {
681 //ignored
682 }
683 super.onDestroy();
684 }
685
686 public void toggleScreenEventReceiver() {
687 if (awayWhenScreenOff()) {
688 final IntentFilter filter = new IntentFilter(Intent.ACTION_SCREEN_ON);
689 filter.addAction(Intent.ACTION_SCREEN_OFF);
690 registerReceiver(this.mEventReceiver, filter);
691 } else {
692 try {
693 unregisterReceiver(this.mEventReceiver);
694 } catch (IllegalArgumentException e) {
695 //ignored
696 }
697 }
698 }
699
700 public void toggleForegroundService() {
701 if (getPreferences().getBoolean("keep_foreground_service", false)) {
702 startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
703 } else {
704 stopForeground(true);
705 }
706 }
707
708 @Override
709 public void onTaskRemoved(final Intent rootIntent) {
710 super.onTaskRemoved(rootIntent);
711 if (!getPreferences().getBoolean("keep_foreground_service",false)) {
712 this.logoutAndSave();
713 }
714 }
715
716 private void logoutAndSave() {
717 for (final Account account : accounts) {
718 databaseBackend.writeRoster(account.getRoster());
719 if (account.getXmppConnection() != null) {
720 disconnect(account, false);
721 }
722 }
723 Context context = getApplicationContext();
724 AlarmManager alarmManager = (AlarmManager) context
725 .getSystemService(Context.ALARM_SERVICE);
726 Intent intent = new Intent(context, EventReceiver.class);
727 alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
728 Log.d(Config.LOGTAG, "good bye");
729 stopSelf();
730 }
731
732 protected void scheduleWakeUpCall(int seconds, int requestCode) {
733 final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000;
734
735 Context context = getApplicationContext();
736 AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
737
738 Intent intent = new Intent(context, EventReceiver.class);
739 intent.setAction("ping");
740 PendingIntent alarmIntent = PendingIntent.getBroadcast(context, requestCode, intent, 0);
741 alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, alarmIntent);
742 }
743
744 public XmppConnection createConnection(final Account account) {
745 final SharedPreferences sharedPref = getPreferences();
746 account.setResource(sharedPref.getString("resource", "mobile")
747 .toLowerCase(Locale.getDefault()));
748 final XmppConnection connection = new XmppConnection(account, this);
749 connection.setOnMessagePacketReceivedListener(this.mMessageParser);
750 connection.setOnStatusChangedListener(this.statusListener);
751 connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
752 connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
753 connection.setOnJinglePacketReceivedListener(this.jingleListener);
754 connection.setOnBindListener(this.mOnBindListener);
755 connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
756 connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
757 return connection;
758 }
759
760 public void sendChatState(Conversation conversation) {
761 if (sendChatStates()) {
762 MessagePacket packet = mMessageGenerator.generateChatState(conversation);
763 sendMessagePacket(conversation.getAccount(), packet);
764 }
765 }
766
767 private void sendFileMessage(final Message message, final boolean delay) {
768 Log.d(Config.LOGTAG, "send file message");
769 final Account account = message.getConversation().getAccount();
770 final XmppConnection connection = account.getXmppConnection();
771 if (connection != null && connection.getFeatures().httpUpload()) {
772 mHttpConnectionManager.createNewUploadConnection(message, delay);
773 } else {
774 mJingleConnectionManager.createNewConnection(message);
775 }
776 }
777
778 public void sendMessage(final Message message) {
779 sendMessage(message, false, false);
780 }
781
782 private void sendMessage(final Message message, final boolean resend, final boolean delay) {
783 final Account account = message.getConversation().getAccount();
784 final Conversation conversation = message.getConversation();
785 account.deactivateGracePeriod();
786 MessagePacket packet = null;
787 boolean saveInDb = true;
788 message.setStatus(Message.STATUS_WAITING);
789
790 if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) {
791 message.getConversation().endOtrIfNeeded();
792 message.getConversation().findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR,
793 new Conversation.OnMessageFound() {
794 @Override
795 public void onMessageFound(Message message) {
796 markMessage(message,Message.STATUS_SEND_FAILED);
797 }
798 });
799 }
800
801 if (account.isOnlineAndConnected()) {
802 switch (message.getEncryption()) {
803 case Message.ENCRYPTION_NONE:
804 if (message.needsUploading()) {
805 if (account.httpUploadAvailable() || message.fixCounterpart()) {
806 this.sendFileMessage(message,delay);
807 } else {
808 break;
809 }
810 } else {
811 packet = mMessageGenerator.generateChat(message);
812 }
813 break;
814 case Message.ENCRYPTION_PGP:
815 case Message.ENCRYPTION_DECRYPTED:
816 if (message.needsUploading()) {
817 if (account.httpUploadAvailable() || message.fixCounterpart()) {
818 this.sendFileMessage(message,delay);
819 } else {
820 break;
821 }
822 } else {
823 packet = mMessageGenerator.generatePgpChat(message);
824 }
825 break;
826 case Message.ENCRYPTION_OTR:
827 SessionImpl otrSession = conversation.getOtrSession();
828 if (otrSession != null && otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
829 try {
830 message.setCounterpart(Jid.fromSessionID(otrSession.getSessionID()));
831 } catch (InvalidJidException e) {
832 break;
833 }
834 if (message.needsUploading()) {
835 mJingleConnectionManager.createNewConnection(message);
836 } else {
837 packet = mMessageGenerator.generateOtrChat(message);
838 }
839 } else if (otrSession == null) {
840 if (message.fixCounterpart()) {
841 conversation.startOtrSession(message.getCounterpart().getResourcepart(), true);
842 } else {
843 break;
844 }
845 }
846 break;
847 case Message.ENCRYPTION_AXOLOTL:
848 message.setAxolotlFingerprint(account.getAxolotlService().getOwnFingerprint());
849 if (message.needsUploading()) {
850 if (account.httpUploadAvailable() || message.fixCounterpart()) {
851 this.sendFileMessage(message,delay);
852 } else {
853 break;
854 }
855 } else {
856 XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message);
857 if (axolotlMessage == null) {
858 account.getAxolotlService().preparePayloadMessage(message, delay);
859 } else {
860 packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
861 }
862 }
863 break;
864
865 }
866 if (packet != null) {
867 if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) {
868 message.setStatus(Message.STATUS_UNSEND);
869 } else {
870 message.setStatus(Message.STATUS_SEND);
871 }
872 }
873 } else {
874 switch(message.getEncryption()) {
875 case Message.ENCRYPTION_DECRYPTED:
876 if (!message.needsUploading()) {
877 String pgpBody = message.getEncryptedBody();
878 String decryptedBody = message.getBody();
879 message.setBody(pgpBody);
880 message.setEncryption(Message.ENCRYPTION_PGP);
881 databaseBackend.createMessage(message);
882 saveInDb = false;
883 message.setBody(decryptedBody);
884 message.setEncryption(Message.ENCRYPTION_DECRYPTED);
885 }
886 break;
887 case Message.ENCRYPTION_OTR:
888 if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) {
889 conversation.startOtrSession(message.getCounterpart().getResourcepart(), false);
890 }
891 break;
892 case Message.ENCRYPTION_AXOLOTL:
893 message.setAxolotlFingerprint(account.getAxolotlService().getOwnFingerprint());
894 break;
895 }
896 }
897
898 if (resend) {
899 if (packet != null) {
900 if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) {
901 markMessage(message,Message.STATUS_UNSEND);
902 } else {
903 markMessage(message,Message.STATUS_SEND);
904 }
905 }
906 } else {
907 conversation.add(message);
908 if (saveInDb && (message.getEncryption() == Message.ENCRYPTION_NONE || saveEncryptedMessages())) {
909 databaseBackend.createMessage(message);
910 }
911 updateConversationUi();
912 }
913 if (packet != null) {
914 if (delay) {
915 mMessageGenerator.addDelay(packet,message.getTimeSent());
916 }
917 if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
918 if (this.sendChatStates()) {
919 packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
920 }
921 }
922 sendMessagePacket(account, packet);
923 }
924 }
925
926 private void sendUnsentMessages(final Conversation conversation) {
927 conversation.findWaitingMessages(new Conversation.OnMessageFound() {
928
929 @Override
930 public void onMessageFound(Message message) {
931 resendMessage(message, true);
932 }
933 });
934 }
935
936 public void resendMessage(final Message message, final boolean delay) {
937 sendMessage(message, true, delay);
938 }
939
940 public void fetchRosterFromServer(final Account account) {
941 final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
942 if (!"".equals(account.getRosterVersion())) {
943 Log.d(Config.LOGTAG, account.getJid().toBareJid()
944 + ": fetching roster version " + account.getRosterVersion());
945 } else {
946 Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
947 }
948 iqPacket.query(Xmlns.ROSTER).setAttribute("ver", account.getRosterVersion());
949 sendIqPacket(account, iqPacket, mIqParser);
950 }
951
952 public void fetchBookmarks(final Account account) {
953 final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
954 final Element query = iqPacket.query("jabber:iq:private");
955 query.addChild("storage", "storage:bookmarks");
956 final OnIqPacketReceived callback = new OnIqPacketReceived() {
957
958 @Override
959 public void onIqPacketReceived(final Account account, final IqPacket packet) {
960 if (packet.getType() == IqPacket.TYPE.RESULT) {
961 final Element query = packet.query();
962 final List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
963 final Element storage = query.findChild("storage", "storage:bookmarks");
964 if (storage != null) {
965 for (final Element item : storage.getChildren()) {
966 if (item.getName().equals("conference")) {
967 final Bookmark bookmark = Bookmark.parse(item, account);
968 bookmarks.add(bookmark);
969 Conversation conversation = find(bookmark);
970 if (conversation != null) {
971 conversation.setBookmark(bookmark);
972 } else if (bookmark.autojoin() && bookmark.getJid() != null) {
973 conversation = findOrCreateConversation(
974 account, bookmark.getJid(), true);
975 conversation.setBookmark(bookmark);
976 joinMuc(conversation);
977 }
978 }
979 }
980 }
981 account.setBookmarks(bookmarks);
982 } else {
983 Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not fetch bookmarks");
984 }
985 }
986 };
987 sendIqPacket(account, iqPacket, callback);
988 }
989
990 public void pushBookmarks(Account account) {
991 IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
992 Element query = iqPacket.query("jabber:iq:private");
993 Element storage = query.addChild("storage", "storage:bookmarks");
994 for (Bookmark bookmark : account.getBookmarks()) {
995 storage.addChild(bookmark);
996 }
997 sendIqPacket(account, iqPacket, mDefaultIqHandler);
998 }
999
1000 public void onPhoneContactsLoaded(final List<Bundle> phoneContacts) {
1001 if (mPhoneContactMergerThread != null) {
1002 mPhoneContactMergerThread.interrupt();
1003 }
1004 mPhoneContactMergerThread = new Thread(new Runnable() {
1005 @Override
1006 public void run() {
1007 Log.d(Config.LOGTAG,"start merging phone contacts with roster");
1008 for (Account account : accounts) {
1009 List<Contact> withSystemAccounts = account.getRoster().getWithSystemAccounts();
1010 for (Bundle phoneContact : phoneContacts) {
1011 if (Thread.interrupted()) {
1012 Log.d(Config.LOGTAG,"interrupted merging phone contacts");
1013 return;
1014 }
1015 Jid jid;
1016 try {
1017 jid = Jid.fromString(phoneContact.getString("jid"));
1018 } catch (final InvalidJidException e) {
1019 continue;
1020 }
1021 final Contact contact = account.getRoster().getContact(jid);
1022 String systemAccount = phoneContact.getInt("phoneid")
1023 + "#"
1024 + phoneContact.getString("lookup");
1025 contact.setSystemAccount(systemAccount);
1026 if (contact.setPhotoUri(phoneContact.getString("photouri"))) {
1027 getAvatarService().clear(contact);
1028 }
1029 contact.setSystemName(phoneContact.getString("displayname"));
1030 withSystemAccounts.remove(contact);
1031 }
1032 for(Contact contact : withSystemAccounts) {
1033 contact.setSystemAccount(null);
1034 contact.setSystemName(null);
1035 if (contact.setPhotoUri(null)) {
1036 getAvatarService().clear(contact);
1037 }
1038 }
1039 }
1040 Log.d(Config.LOGTAG,"finished merging phone contacts");
1041 updateAccountUi();
1042 }
1043 });
1044 mPhoneContactMergerThread.start();
1045 }
1046
1047 private void restoreFromDatabase() {
1048 synchronized (this.conversations) {
1049 final Map<String, Account> accountLookupTable = new Hashtable<>();
1050 for (Account account : this.accounts) {
1051 accountLookupTable.put(account.getUuid(), account);
1052 }
1053 this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
1054 for (Conversation conversation : this.conversations) {
1055 Account account = accountLookupTable.get(conversation.getAccountUuid());
1056 conversation.setAccount(account);
1057 }
1058 Runnable runnable =new Runnable() {
1059 @Override
1060 public void run() {
1061 Log.d(Config.LOGTAG,"restoring roster");
1062 for(Account account : accounts) {
1063 databaseBackend.readRoster(account.getRoster());
1064 account.initAccountServices(XmppConnectionService.this);
1065 }
1066 getBitmapCache().evictAll();
1067 Looper.prepare();
1068 PhoneHelper.loadPhoneContacts(getApplicationContext(),
1069 new CopyOnWriteArrayList<Bundle>(),
1070 XmppConnectionService.this);
1071 Log.d(Config.LOGTAG,"restoring messages");
1072 for (Conversation conversation : conversations) {
1073 conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
1074 checkDeletedFiles(conversation);
1075 }
1076 mRestoredFromDatabase = true;
1077 Log.d(Config.LOGTAG,"restored all messages");
1078 updateConversationUi();
1079 }
1080 };
1081 mDatabaseExecutor.execute(runnable);
1082 }
1083 }
1084
1085 public List<Conversation> getConversations() {
1086 return this.conversations;
1087 }
1088
1089 private void checkDeletedFiles(Conversation conversation) {
1090 conversation.findMessagesWithFiles(new Conversation.OnMessageFound() {
1091
1092 @Override
1093 public void onMessageFound(Message message) {
1094 if (!getFileBackend().isFileAvailable(message)) {
1095 message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
1096 final int s = message.getStatus();
1097 if(s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) {
1098 markMessage(message,Message.STATUS_SEND_FAILED);
1099 }
1100 }
1101 }
1102 });
1103 }
1104
1105 private void markFileDeleted(String uuid) {
1106 for (Conversation conversation : getConversations()) {
1107 Message message = conversation.findMessageWithFileAndUuid(uuid);
1108 if (message != null) {
1109 if (!getFileBackend().isFileAvailable(message)) {
1110 message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
1111 final int s = message.getStatus();
1112 if(s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) {
1113 markMessage(message,Message.STATUS_SEND_FAILED);
1114 } else {
1115 updateConversationUi();
1116 }
1117 }
1118 return;
1119 }
1120 }
1121 }
1122
1123 public void populateWithOrderedConversations(final List<Conversation> list) {
1124 populateWithOrderedConversations(list, true);
1125 }
1126
1127 public void populateWithOrderedConversations(final List<Conversation> list, boolean includeNoFileUpload) {
1128 list.clear();
1129 if (includeNoFileUpload) {
1130 list.addAll(getConversations());
1131 } else {
1132 for (Conversation conversation : getConversations()) {
1133 if (conversation.getMode() == Conversation.MODE_SINGLE
1134 || conversation.getAccount().httpUploadAvailable()) {
1135 list.add(conversation);
1136 }
1137 }
1138 }
1139 Collections.sort(list, new Comparator<Conversation>() {
1140 @Override
1141 public int compare(Conversation lhs, Conversation rhs) {
1142 Message left = lhs.getLatestMessage();
1143 Message right = rhs.getLatestMessage();
1144 if (left.getTimeSent() > right.getTimeSent()) {
1145 return -1;
1146 } else if (left.getTimeSent() < right.getTimeSent()) {
1147 return 1;
1148 } else {
1149 return 0;
1150 }
1151 }
1152 });
1153 }
1154
1155 public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
1156 if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation,callback)) {
1157 return;
1158 }
1159 Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp));
1160 Runnable runnable = new Runnable() {
1161 @Override
1162 public void run() {
1163 final Account account = conversation.getAccount();
1164 List<Message> messages = databaseBackend.getMessages(conversation, 50,timestamp);
1165 if (messages.size() > 0) {
1166 conversation.addAll(0, messages);
1167 checkDeletedFiles(conversation);
1168 callback.onMoreMessagesLoaded(messages.size(), conversation);
1169 } else if (conversation.hasMessagesLeftOnServer()
1170 && account.isOnlineAndConnected()) {
1171 if ((conversation.getMode() == Conversation.MODE_SINGLE && account.getXmppConnection().getFeatures().mam())
1172 || (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().mamSupport())) {
1173 MessageArchiveService.Query query = getMessageArchiveService().query(conversation,0,timestamp - 1);
1174 if (query != null) {
1175 query.setCallback(callback);
1176 }
1177 callback.informUser(R.string.fetching_history_from_server);
1178 }
1179 }
1180 }
1181 };
1182 mDatabaseExecutor.execute(runnable);
1183 }
1184
1185 public List<Account> getAccounts() {
1186 return this.accounts;
1187 }
1188
1189 public Conversation find(final Iterable<Conversation> haystack, final Contact contact) {
1190 for (final Conversation conversation : haystack) {
1191 if (conversation.getContact() == contact) {
1192 return conversation;
1193 }
1194 }
1195 return null;
1196 }
1197
1198 public Conversation find(final Iterable<Conversation> haystack, final Account account, final Jid jid) {
1199 if (jid == null) {
1200 return null;
1201 }
1202 for (final Conversation conversation : haystack) {
1203 if ((account == null || conversation.getAccount() == account)
1204 && (conversation.getJid().toBareJid().equals(jid.toBareJid()))) {
1205 return conversation;
1206 }
1207 }
1208 return null;
1209 }
1210
1211 public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc) {
1212 return this.findOrCreateConversation(account, jid, muc, null);
1213 }
1214
1215 public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final MessageArchiveService.Query query) {
1216 synchronized (this.conversations) {
1217 Conversation conversation = find(account, jid);
1218 if (conversation != null) {
1219 return conversation;
1220 }
1221 conversation = databaseBackend.findConversation(account, jid);
1222 if (conversation != null) {
1223 conversation.setStatus(Conversation.STATUS_AVAILABLE);
1224 conversation.setAccount(account);
1225 if (muc) {
1226 conversation.setMode(Conversation.MODE_MULTI);
1227 conversation.setContactJid(jid);
1228 } else {
1229 conversation.setMode(Conversation.MODE_SINGLE);
1230 conversation.setContactJid(jid.toBareJid());
1231 }
1232 conversation.setNextEncryption(-1);
1233 conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
1234 this.databaseBackend.updateConversation(conversation);
1235 } else {
1236 String conversationName;
1237 Contact contact = account.getRoster().getContact(jid);
1238 if (contact != null) {
1239 conversationName = contact.getDisplayName();
1240 } else {
1241 conversationName = jid.getLocalpart();
1242 }
1243 if (muc) {
1244 conversation = new Conversation(conversationName, account, jid,
1245 Conversation.MODE_MULTI);
1246 } else {
1247 conversation = new Conversation(conversationName, account, jid.toBareJid(),
1248 Conversation.MODE_SINGLE);
1249 }
1250 this.databaseBackend.createConversation(conversation);
1251 }
1252 if (account.getXmppConnection() != null
1253 && account.getXmppConnection().getFeatures().mam()
1254 && !muc) {
1255 if (query == null) {
1256 this.mMessageArchiveService.query(conversation);
1257 } else {
1258 if (query.getConversation() == null) {
1259 this.mMessageArchiveService.query(conversation, query.getStart());
1260 }
1261 }
1262 }
1263 checkDeletedFiles(conversation);
1264 this.conversations.add(conversation);
1265 updateConversationUi();
1266 return conversation;
1267 }
1268 }
1269
1270 public void archiveConversation(Conversation conversation) {
1271 getNotificationService().clear(conversation);
1272 conversation.setStatus(Conversation.STATUS_ARCHIVED);
1273 conversation.setNextEncryption(-1);
1274 synchronized (this.conversations) {
1275 if (conversation.getMode() == Conversation.MODE_MULTI) {
1276 if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1277 Bookmark bookmark = conversation.getBookmark();
1278 if (bookmark != null && bookmark.autojoin()) {
1279 bookmark.setAutojoin(false);
1280 pushBookmarks(bookmark.getAccount());
1281 }
1282 }
1283 leaveMuc(conversation);
1284 } else {
1285 conversation.endOtrIfNeeded();
1286 }
1287 this.databaseBackend.updateConversation(conversation);
1288 this.conversations.remove(conversation);
1289 updateConversationUi();
1290 }
1291 }
1292
1293 public void createAccount(final Account account) {
1294 account.initAccountServices(this);
1295 databaseBackend.createAccount(account);
1296 this.accounts.add(account);
1297 this.reconnectAccountInBackground(account);
1298 updateAccountUi();
1299 }
1300
1301 public void createAccountFromKey(final String alias, final OnAccountCreated callback) {
1302 new Thread(new Runnable() {
1303 @Override
1304 public void run() {
1305 try {
1306 X509Certificate[] chain = KeyChain.getCertificateChain(XmppConnectionService.this, alias);
1307 PrivateKey key = KeyChain.getPrivateKey(XmppConnectionService.this, alias);
1308 X500Name x500name = new JcaX509CertificateHolder(chain[0]).getSubject();
1309 String email = IETFUtils.valueToString(x500name.getRDNs(BCStyle.EmailAddress)[0].getFirst().getValue());
1310 String name = IETFUtils.valueToString(x500name.getRDNs(BCStyle.CN)[0].getFirst().getValue());
1311 Jid jid = Jid.fromString(email);
1312 if (findAccountByJid(jid) == null) {
1313 Account account = new Account(jid, "");
1314 account.setPrivateKeyAlias(alias);
1315 account.setOption(Account.OPTION_DISABLED, true);
1316 createAccount(account);
1317 callback.onAccountCreated(account);
1318 } else {
1319 callback.informUser(R.string.account_already_exists);
1320 }
1321 } catch (KeyChainException e) {
1322 callback.informUser(R.string.unable_to_parse_certificate);
1323 } catch (InterruptedException e) {
1324 callback.informUser(R.string.unable_to_parse_certificate);
1325 e.printStackTrace();
1326 } catch (CertificateEncodingException e) {
1327 callback.informUser(R.string.unable_to_parse_certificate);
1328 e.printStackTrace();
1329 } catch (InvalidJidException e) {
1330 callback.informUser(R.string.unable_to_parse_certificate);
1331 e.printStackTrace();
1332 }
1333 }
1334 }).start();
1335
1336 }
1337
1338 public void updateAccount(final Account account) {
1339 this.statusListener.onStatusChanged(account);
1340 databaseBackend.updateAccount(account);
1341 reconnectAccount(account, false, true);
1342 updateAccountUi();
1343 getNotificationService().updateErrorNotification();
1344 }
1345
1346 public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) {
1347 final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword);
1348 sendIqPacket(account, iq, new OnIqPacketReceived() {
1349 @Override
1350 public void onIqPacketReceived(final Account account, final IqPacket packet) {
1351 if (packet.getType() == IqPacket.TYPE.RESULT) {
1352 account.setPassword(newPassword);
1353 databaseBackend.updateAccount(account);
1354 callback.onPasswordChangeSucceeded();
1355 } else {
1356 callback.onPasswordChangeFailed();
1357 }
1358 }
1359 });
1360 }
1361
1362 public void deleteAccount(final Account account) {
1363 synchronized (this.conversations) {
1364 for (final Conversation conversation : conversations) {
1365 if (conversation.getAccount() == account) {
1366 if (conversation.getMode() == Conversation.MODE_MULTI) {
1367 leaveMuc(conversation);
1368 } else if (conversation.getMode() == Conversation.MODE_SINGLE) {
1369 conversation.endOtrIfNeeded();
1370 }
1371 conversations.remove(conversation);
1372 }
1373 }
1374 if (account.getXmppConnection() != null) {
1375 this.disconnect(account, true);
1376 }
1377 databaseBackend.deleteAccount(account);
1378 this.accounts.remove(account);
1379 updateAccountUi();
1380 getNotificationService().updateErrorNotification();
1381 }
1382 }
1383
1384 public void setOnConversationListChangedListener(OnConversationUpdate listener) {
1385 synchronized (this) {
1386 if (checkListeners()) {
1387 switchToForeground();
1388 }
1389 this.mOnConversationUpdate = listener;
1390 this.mNotificationService.setIsInForeground(true);
1391 if (this.convChangedListenerCount < 2) {
1392 this.convChangedListenerCount++;
1393 }
1394 }
1395 }
1396
1397 public void removeOnConversationListChangedListener() {
1398 synchronized (this) {
1399 this.convChangedListenerCount--;
1400 if (this.convChangedListenerCount <= 0) {
1401 this.convChangedListenerCount = 0;
1402 this.mOnConversationUpdate = null;
1403 this.mNotificationService.setIsInForeground(false);
1404 if (checkListeners()) {
1405 switchToBackground();
1406 }
1407 }
1408 }
1409 }
1410
1411 public void setOnShowErrorToastListener(OnShowErrorToast onShowErrorToast) {
1412 synchronized (this) {
1413 if (checkListeners()) {
1414 switchToForeground();
1415 }
1416 this.mOnShowErrorToast = onShowErrorToast;
1417 if (this.showErrorToastListenerCount < 2) {
1418 this.showErrorToastListenerCount++;
1419 }
1420 }
1421 this.mOnShowErrorToast = onShowErrorToast;
1422 }
1423
1424 public void removeOnShowErrorToastListener() {
1425 synchronized (this) {
1426 this.showErrorToastListenerCount--;
1427 if (this.showErrorToastListenerCount <= 0) {
1428 this.showErrorToastListenerCount = 0;
1429 this.mOnShowErrorToast = null;
1430 if (checkListeners()) {
1431 switchToBackground();
1432 }
1433 }
1434 }
1435 }
1436
1437 public void setOnAccountListChangedListener(OnAccountUpdate listener) {
1438 synchronized (this) {
1439 if (checkListeners()) {
1440 switchToForeground();
1441 }
1442 this.mOnAccountUpdate = listener;
1443 if (this.accountChangedListenerCount < 2) {
1444 this.accountChangedListenerCount++;
1445 }
1446 }
1447 }
1448
1449 public void removeOnAccountListChangedListener() {
1450 synchronized (this) {
1451 this.accountChangedListenerCount--;
1452 if (this.accountChangedListenerCount <= 0) {
1453 this.mOnAccountUpdate = null;
1454 this.accountChangedListenerCount = 0;
1455 if (checkListeners()) {
1456 switchToBackground();
1457 }
1458 }
1459 }
1460 }
1461
1462 public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
1463 synchronized (this) {
1464 if (checkListeners()) {
1465 switchToForeground();
1466 }
1467 this.mOnRosterUpdate = listener;
1468 if (this.rosterChangedListenerCount < 2) {
1469 this.rosterChangedListenerCount++;
1470 }
1471 }
1472 }
1473
1474 public void removeOnRosterUpdateListener() {
1475 synchronized (this) {
1476 this.rosterChangedListenerCount--;
1477 if (this.rosterChangedListenerCount <= 0) {
1478 this.rosterChangedListenerCount = 0;
1479 this.mOnRosterUpdate = null;
1480 if (checkListeners()) {
1481 switchToBackground();
1482 }
1483 }
1484 }
1485 }
1486
1487 public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
1488 synchronized (this) {
1489 if (checkListeners()) {
1490 switchToForeground();
1491 }
1492 this.mOnUpdateBlocklist = listener;
1493 if (this.updateBlocklistListenerCount < 2) {
1494 this.updateBlocklistListenerCount++;
1495 }
1496 }
1497 }
1498
1499 public void removeOnUpdateBlocklistListener() {
1500 synchronized (this) {
1501 this.updateBlocklistListenerCount--;
1502 if (this.updateBlocklistListenerCount <= 0) {
1503 this.updateBlocklistListenerCount = 0;
1504 this.mOnUpdateBlocklist = null;
1505 if (checkListeners()) {
1506 switchToBackground();
1507 }
1508 }
1509 }
1510 }
1511
1512 public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
1513 synchronized (this) {
1514 if (checkListeners()) {
1515 switchToForeground();
1516 }
1517 this.mOnKeyStatusUpdated = listener;
1518 if (this.keyStatusUpdatedListenerCount < 2) {
1519 this.keyStatusUpdatedListenerCount++;
1520 }
1521 }
1522 }
1523
1524 public void removeOnNewKeysAvailableListener() {
1525 synchronized (this) {
1526 this.keyStatusUpdatedListenerCount--;
1527 if (this.keyStatusUpdatedListenerCount <= 0) {
1528 this.keyStatusUpdatedListenerCount = 0;
1529 this.mOnKeyStatusUpdated = null;
1530 if (checkListeners()) {
1531 switchToBackground();
1532 }
1533 }
1534 }
1535 }
1536
1537 public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
1538 synchronized (this) {
1539 if (checkListeners()) {
1540 switchToForeground();
1541 }
1542 this.mOnMucRosterUpdate = listener;
1543 if (this.mucRosterChangedListenerCount < 2) {
1544 this.mucRosterChangedListenerCount++;
1545 }
1546 }
1547 }
1548
1549 public void removeOnMucRosterUpdateListener() {
1550 synchronized (this) {
1551 this.mucRosterChangedListenerCount--;
1552 if (this.mucRosterChangedListenerCount <= 0) {
1553 this.mucRosterChangedListenerCount = 0;
1554 this.mOnMucRosterUpdate = null;
1555 if (checkListeners()) {
1556 switchToBackground();
1557 }
1558 }
1559 }
1560 }
1561
1562 private boolean checkListeners() {
1563 return (this.mOnAccountUpdate == null
1564 && this.mOnConversationUpdate == null
1565 && this.mOnRosterUpdate == null
1566 && this.mOnUpdateBlocklist == null
1567 && this.mOnShowErrorToast == null
1568 && this.mOnKeyStatusUpdated == null);
1569 }
1570
1571 private void switchToForeground() {
1572 for (Account account : getAccounts()) {
1573 if (account.getStatus() == Account.State.ONLINE) {
1574 XmppConnection connection = account.getXmppConnection();
1575 if (connection != null && connection.getFeatures().csi()) {
1576 connection.sendActive();
1577 }
1578 }
1579 }
1580 Log.d(Config.LOGTAG, "app switched into foreground");
1581 }
1582
1583 private void switchToBackground() {
1584 for (Account account : getAccounts()) {
1585 if (account.getStatus() == Account.State.ONLINE) {
1586 XmppConnection connection = account.getXmppConnection();
1587 if (connection != null && connection.getFeatures().csi()) {
1588 connection.sendInactive();
1589 }
1590 }
1591 }
1592 for(Conversation conversation : getConversations()) {
1593 conversation.setIncomingChatState(ChatState.ACTIVE);
1594 }
1595 this.mNotificationService.setIsInForeground(false);
1596 Log.d(Config.LOGTAG, "app switched into background");
1597 }
1598
1599 private void connectMultiModeConversations(Account account) {
1600 List<Conversation> conversations = getConversations();
1601 for (Conversation conversation : conversations) {
1602 if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) {
1603 joinMuc(conversation,true);
1604 }
1605 }
1606 }
1607
1608 public void joinMuc(Conversation conversation) {
1609 joinMuc(conversation, false);
1610 }
1611
1612 private void joinMuc(Conversation conversation, boolean now) {
1613 Account account = conversation.getAccount();
1614 account.pendingConferenceJoins.remove(conversation);
1615 account.pendingConferenceLeaves.remove(conversation);
1616 if (account.getStatus() == Account.State.ONLINE || now) {
1617 conversation.resetMucOptions();
1618 fetchConferenceConfiguration(conversation, new OnConferenceConfigurationFetched() {
1619 @Override
1620 public void onConferenceConfigurationFetched(Conversation conversation) {
1621 Account account = conversation.getAccount();
1622 final String nick = conversation.getMucOptions().getProposedNick();
1623 final Jid joinJid = conversation.getMucOptions().createJoinJid(nick);
1624 if (joinJid == null) {
1625 return; //safety net
1626 }
1627 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString());
1628 PresencePacket packet = new PresencePacket();
1629 packet.setFrom(conversation.getAccount().getJid());
1630 packet.setTo(joinJid);
1631 Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
1632 if (conversation.getMucOptions().getPassword() != null) {
1633 x.addChild("password").setContent(conversation.getMucOptions().getPassword());
1634 }
1635
1636 if (conversation.getMucOptions().mamSupport()) {
1637 // Use MAM instead of the limited muc history to get history
1638 x.addChild("history").setAttribute("maxchars", "0");
1639 } else {
1640 // Fallback to muc history
1641 x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted()));
1642 }
1643 String sig = account.getPgpSignature();
1644 if (sig != null) {
1645 packet.addChild("status").setContent("online");
1646 packet.addChild("x", "jabber:x:signed").setContent(sig);
1647 }
1648 sendPresencePacket(account, packet);
1649 fetchConferenceConfiguration(conversation);
1650 if (!joinJid.equals(conversation.getJid())) {
1651 conversation.setContactJid(joinJid);
1652 databaseBackend.updateConversation(conversation);
1653 }
1654 conversation.setHasMessagesLeftOnServer(false);
1655 if (conversation.getMucOptions().mamSupport()) {
1656 getMessageArchiveService().catchupMUC(conversation);
1657 }
1658 }
1659 });
1660
1661 } else {
1662 account.pendingConferenceJoins.add(conversation);
1663 }
1664 }
1665
1666 public void providePasswordForMuc(Conversation conversation, String password) {
1667 if (conversation.getMode() == Conversation.MODE_MULTI) {
1668 conversation.getMucOptions().setPassword(password);
1669 if (conversation.getBookmark() != null) {
1670 conversation.getBookmark().setAutojoin(true);
1671 pushBookmarks(conversation.getAccount());
1672 }
1673 databaseBackend.updateConversation(conversation);
1674 joinMuc(conversation);
1675 }
1676 }
1677
1678 public void renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
1679 final MucOptions options = conversation.getMucOptions();
1680 final Jid joinJid = options.createJoinJid(nick);
1681 if (options.online()) {
1682 Account account = conversation.getAccount();
1683 options.setOnRenameListener(new OnRenameListener() {
1684
1685 @Override
1686 public void onSuccess() {
1687 conversation.setContactJid(joinJid);
1688 databaseBackend.updateConversation(conversation);
1689 Bookmark bookmark = conversation.getBookmark();
1690 if (bookmark != null) {
1691 bookmark.setNick(nick);
1692 pushBookmarks(bookmark.getAccount());
1693 }
1694 callback.success(conversation);
1695 }
1696
1697 @Override
1698 public void onFailure() {
1699 callback.error(R.string.nick_in_use, conversation);
1700 }
1701 });
1702
1703 PresencePacket packet = new PresencePacket();
1704 packet.setTo(joinJid);
1705 packet.setFrom(conversation.getAccount().getJid());
1706
1707 String sig = account.getPgpSignature();
1708 if (sig != null) {
1709 packet.addChild("status").setContent("online");
1710 packet.addChild("x", "jabber:x:signed").setContent(sig);
1711 }
1712 sendPresencePacket(account, packet);
1713 } else {
1714 conversation.setContactJid(joinJid);
1715 databaseBackend.updateConversation(conversation);
1716 if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1717 Bookmark bookmark = conversation.getBookmark();
1718 if (bookmark != null) {
1719 bookmark.setNick(nick);
1720 pushBookmarks(bookmark.getAccount());
1721 }
1722 joinMuc(conversation);
1723 }
1724 }
1725 }
1726
1727 public void leaveMuc(Conversation conversation) {
1728 Account account = conversation.getAccount();
1729 account.pendingConferenceJoins.remove(conversation);
1730 account.pendingConferenceLeaves.remove(conversation);
1731 if (account.getStatus() == Account.State.ONLINE) {
1732 PresencePacket packet = new PresencePacket();
1733 packet.setTo(conversation.getJid());
1734 packet.setFrom(conversation.getAccount().getJid());
1735 packet.setAttribute("type", "unavailable");
1736 sendPresencePacket(conversation.getAccount(), packet);
1737 conversation.getMucOptions().setOffline();
1738 conversation.deregisterWithBookmark();
1739 Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
1740 + ": leaving muc " + conversation.getJid());
1741 } else {
1742 account.pendingConferenceLeaves.add(conversation);
1743 }
1744 }
1745
1746 private String findConferenceServer(final Account account) {
1747 String server;
1748 if (account.getXmppConnection() != null) {
1749 server = account.getXmppConnection().getMucServer();
1750 if (server != null) {
1751 return server;
1752 }
1753 }
1754 for (Account other : getAccounts()) {
1755 if (other != account && other.getXmppConnection() != null) {
1756 server = other.getXmppConnection().getMucServer();
1757 if (server != null) {
1758 return server;
1759 }
1760 }
1761 }
1762 return null;
1763 }
1764
1765 public void createAdhocConference(final Account account, final Iterable<Jid> jids, final UiCallback<Conversation> callback) {
1766 Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString());
1767 if (account.getStatus() == Account.State.ONLINE) {
1768 try {
1769 String server = findConferenceServer(account);
1770 if (server == null) {
1771 if (callback != null) {
1772 callback.error(R.string.no_conference_server_found, null);
1773 }
1774 return;
1775 }
1776 String name = new BigInteger(75, getRNG()).toString(32);
1777 Jid jid = Jid.fromParts(name, server, null);
1778 final Conversation conversation = findOrCreateConversation(account, jid, true);
1779 joinMuc(conversation);
1780 Bundle options = new Bundle();
1781 options.putString("muc#roomconfig_persistentroom", "1");
1782 options.putString("muc#roomconfig_membersonly", "1");
1783 options.putString("muc#roomconfig_publicroom", "0");
1784 options.putString("muc#roomconfig_whois", "anyone");
1785 pushConferenceConfiguration(conversation, options, new OnConferenceOptionsPushed() {
1786 @Override
1787 public void onPushSucceeded() {
1788 for (Jid invite : jids) {
1789 invite(conversation, invite);
1790 }
1791 if (account.countPresences() > 1) {
1792 directInvite(conversation, account.getJid().toBareJid());
1793 }
1794 if (callback != null) {
1795 callback.success(conversation);
1796 }
1797 }
1798
1799 @Override
1800 public void onPushFailed() {
1801 if (callback != null) {
1802 callback.error(R.string.conference_creation_failed, conversation);
1803 }
1804 }
1805 });
1806
1807 } catch (InvalidJidException e) {
1808 if (callback != null) {
1809 callback.error(R.string.conference_creation_failed, null);
1810 }
1811 }
1812 } else {
1813 if (callback != null) {
1814 callback.error(R.string.not_connected_try_again, null);
1815 }
1816 }
1817 }
1818
1819 public void fetchConferenceConfiguration(final Conversation conversation) {
1820 fetchConferenceConfiguration(conversation, null);
1821 }
1822
1823 public void fetchConferenceConfiguration(final Conversation conversation, final OnConferenceConfigurationFetched callback) {
1824 IqPacket request = new IqPacket(IqPacket.TYPE.GET);
1825 request.setTo(conversation.getJid().toBareJid());
1826 request.query("http://jabber.org/protocol/disco#info");
1827 sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
1828 @Override
1829 public void onIqPacketReceived(Account account, IqPacket packet) {
1830 if (packet.getType() == IqPacket.TYPE.RESULT) {
1831 ArrayList<String> features = new ArrayList<>();
1832 for (Element child : packet.query().getChildren()) {
1833 if (child != null && child.getName().equals("feature")) {
1834 String var = child.getAttribute("var");
1835 if (var != null) {
1836 features.add(var);
1837 }
1838 }
1839 }
1840 conversation.getMucOptions().updateFeatures(features);
1841 if (callback != null) {
1842 callback.onConferenceConfigurationFetched(conversation);
1843 }
1844 updateConversationUi();
1845 }
1846 }
1847 });
1848 }
1849
1850 public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConferenceOptionsPushed callback) {
1851 IqPacket request = new IqPacket(IqPacket.TYPE.GET);
1852 request.setTo(conversation.getJid().toBareJid());
1853 request.query("http://jabber.org/protocol/muc#owner");
1854 sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
1855 @Override
1856 public void onIqPacketReceived(Account account, IqPacket packet) {
1857 if (packet.getType() == IqPacket.TYPE.RESULT) {
1858 Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
1859 for (Field field : data.getFields()) {
1860 if (options.containsKey(field.getFieldName())) {
1861 field.setValue(options.getString(field.getFieldName()));
1862 }
1863 }
1864 data.submit();
1865 IqPacket set = new IqPacket(IqPacket.TYPE.SET);
1866 set.setTo(conversation.getJid().toBareJid());
1867 set.query("http://jabber.org/protocol/muc#owner").addChild(data);
1868 sendIqPacket(account, set, new OnIqPacketReceived() {
1869 @Override
1870 public void onIqPacketReceived(Account account, IqPacket packet) {
1871 if (callback != null) {
1872 if (packet.getType() == IqPacket.TYPE.RESULT) {
1873 callback.onPushSucceeded();
1874 } else {
1875 callback.onPushFailed();
1876 }
1877 }
1878 }
1879 });
1880 } else {
1881 if (callback != null) {
1882 callback.onPushFailed();
1883 }
1884 }
1885 }
1886 });
1887 }
1888
1889 public void pushSubjectToConference(final Conversation conference, final String subject) {
1890 MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, subject);
1891 this.sendMessagePacket(conference.getAccount(), packet);
1892 final MucOptions mucOptions = conference.getMucOptions();
1893 final MucOptions.User self = mucOptions.getSelf();
1894 if (!mucOptions.persistent() && self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
1895 Bundle options = new Bundle();
1896 options.putString("muc#roomconfig_persistentroom", "1");
1897 this.pushConferenceConfiguration(conference, options, null);
1898 }
1899 }
1900
1901 public void changeAffiliationInConference(final Conversation conference, Jid user, MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
1902 final Jid jid = user.toBareJid();
1903 IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
1904 sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
1905 @Override
1906 public void onIqPacketReceived(Account account, IqPacket packet) {
1907 if (packet.getType() == IqPacket.TYPE.RESULT) {
1908 callback.onAffiliationChangedSuccessful(jid);
1909 } else {
1910 callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
1911 }
1912 }
1913 });
1914 }
1915
1916 public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
1917 List<Jid> jids = new ArrayList<>();
1918 for (MucOptions.User user : conference.getMucOptions().getUsers()) {
1919 if (user.getAffiliation() == before && user.getJid() != null) {
1920 jids.add(user.getJid());
1921 }
1922 }
1923 IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
1924 sendIqPacket(conference.getAccount(), request, mDefaultIqHandler);
1925 }
1926
1927 public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) {
1928 IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
1929 Log.d(Config.LOGTAG, request.toString());
1930 sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
1931 @Override
1932 public void onIqPacketReceived(Account account, IqPacket packet) {
1933 Log.d(Config.LOGTAG, packet.toString());
1934 if (packet.getType() == IqPacket.TYPE.RESULT) {
1935 callback.onRoleChangedSuccessful(nick);
1936 } else {
1937 callback.onRoleChangeFailed(nick, R.string.could_not_change_role);
1938 }
1939 }
1940 });
1941 }
1942
1943 public void disconnect(Account account, boolean force) {
1944 if ((account.getStatus() == Account.State.ONLINE)
1945 || (account.getStatus() == Account.State.DISABLED)) {
1946 if (!force) {
1947 List<Conversation> conversations = getConversations();
1948 for (Conversation conversation : conversations) {
1949 if (conversation.getAccount() == account) {
1950 if (conversation.getMode() == Conversation.MODE_MULTI) {
1951 leaveMuc(conversation);
1952 } else {
1953 if (conversation.endOtrIfNeeded()) {
1954 Log.d(Config.LOGTAG, account.getJid().toBareJid()
1955 + ": ended otr session with "
1956 + conversation.getJid());
1957 }
1958 }
1959 }
1960 }
1961 sendOfflinePresence(account);
1962 }
1963 account.getXmppConnection().disconnect(force);
1964 }
1965 }
1966
1967 @Override
1968 public IBinder onBind(Intent intent) {
1969 return mBinder;
1970 }
1971
1972 public void updateMessage(Message message) {
1973 databaseBackend.updateMessage(message);
1974 updateConversationUi();
1975 }
1976
1977 protected void syncDirtyContacts(Account account) {
1978 for (Contact contact : account.getRoster().getContacts()) {
1979 if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
1980 pushContactToServer(contact);
1981 }
1982 if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
1983 deleteContactOnServer(contact);
1984 }
1985 }
1986 }
1987
1988 public void createContact(Contact contact) {
1989 SharedPreferences sharedPref = getPreferences();
1990 boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1991 if (autoGrant) {
1992 contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
1993 contact.setOption(Contact.Options.ASKING);
1994 }
1995 pushContactToServer(contact);
1996 }
1997
1998 public void onOtrSessionEstablished(Conversation conversation) {
1999 final Account account = conversation.getAccount();
2000 final Session otrSession = conversation.getOtrSession();
2001 Log.d(Config.LOGTAG,
2002 account.getJid().toBareJid() + " otr session established with "
2003 + conversation.getJid() + "/"
2004 + otrSession.getSessionID().getUserID());
2005 conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
2006
2007 @Override
2008 public void onMessageFound(Message message) {
2009 SessionID id = otrSession.getSessionID();
2010 try {
2011 message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
2012 } catch (InvalidJidException e) {
2013 return;
2014 }
2015 if (message.needsUploading()) {
2016 mJingleConnectionManager.createNewConnection(message);
2017 } else {
2018 MessagePacket outPacket = mMessageGenerator.generateOtrChat(message);
2019 if (outPacket != null) {
2020 mMessageGenerator.addDelay(outPacket, message.getTimeSent());
2021 message.setStatus(Message.STATUS_SEND);
2022 databaseBackend.updateMessage(message);
2023 sendMessagePacket(account, outPacket);
2024 }
2025 }
2026 updateConversationUi();
2027 }
2028 });
2029 }
2030
2031 public boolean renewSymmetricKey(Conversation conversation) {
2032 Account account = conversation.getAccount();
2033 byte[] symmetricKey = new byte[32];
2034 this.mRandom.nextBytes(symmetricKey);
2035 Session otrSession = conversation.getOtrSession();
2036 if (otrSession != null) {
2037 MessagePacket packet = new MessagePacket();
2038 packet.setType(MessagePacket.TYPE_CHAT);
2039 packet.setFrom(account.getJid());
2040 MessageGenerator.addMessageHints(packet);
2041 packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
2042 + otrSession.getSessionID().getUserID());
2043 try {
2044 packet.setBody(otrSession
2045 .transformSending(CryptoHelper.FILETRANSFER
2046 + CryptoHelper.bytesToHex(symmetricKey))[0]);
2047 sendMessagePacket(account, packet);
2048 conversation.setSymmetricKey(symmetricKey);
2049 return true;
2050 } catch (OtrException e) {
2051 return false;
2052 }
2053 }
2054 return false;
2055 }
2056
2057 public void pushContactToServer(final Contact contact) {
2058 contact.resetOption(Contact.Options.DIRTY_DELETE);
2059 contact.setOption(Contact.Options.DIRTY_PUSH);
2060 final Account account = contact.getAccount();
2061 if (account.getStatus() == Account.State.ONLINE) {
2062 final boolean ask = contact.getOption(Contact.Options.ASKING);
2063 final boolean sendUpdates = contact
2064 .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
2065 && contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
2066 final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2067 iq.query(Xmlns.ROSTER).addChild(contact.asElement());
2068 account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
2069 if (sendUpdates) {
2070 sendPresencePacket(account,
2071 mPresenceGenerator.sendPresenceUpdatesTo(contact));
2072 }
2073 if (ask) {
2074 sendPresencePacket(account,
2075 mPresenceGenerator.requestPresenceUpdatesFrom(contact));
2076 }
2077 }
2078 }
2079
2080 public void publishAvatar(final Account account,
2081 final Uri image,
2082 final UiCallback<Avatar> callback) {
2083 final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
2084 final int size = Config.AVATAR_SIZE;
2085 final Avatar avatar = getFileBackend()
2086 .getPepAvatar(image, size, format);
2087 if (avatar != null) {
2088 avatar.height = size;
2089 avatar.width = size;
2090 if (format.equals(Bitmap.CompressFormat.WEBP)) {
2091 avatar.type = "image/webp";
2092 } else if (format.equals(Bitmap.CompressFormat.JPEG)) {
2093 avatar.type = "image/jpeg";
2094 } else if (format.equals(Bitmap.CompressFormat.PNG)) {
2095 avatar.type = "image/png";
2096 }
2097 if (!getFileBackend().save(avatar)) {
2098 callback.error(R.string.error_saving_avatar, avatar);
2099 return;
2100 }
2101 final IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
2102 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2103
2104 @Override
2105 public void onIqPacketReceived(Account account, IqPacket result) {
2106 if (result.getType() == IqPacket.TYPE.RESULT) {
2107 final IqPacket packet = XmppConnectionService.this.mIqGenerator
2108 .publishAvatarMetadata(avatar);
2109 sendIqPacket(account, packet, new OnIqPacketReceived() {
2110 @Override
2111 public void onIqPacketReceived(Account account, IqPacket result) {
2112 if (result.getType() == IqPacket.TYPE.RESULT) {
2113 if (account.setAvatar(avatar.getFilename())) {
2114 getAvatarService().clear(account);
2115 databaseBackend.updateAccount(account);
2116 }
2117 callback.success(avatar);
2118 } else {
2119 callback.error(
2120 R.string.error_publish_avatar_server_reject,
2121 avatar);
2122 }
2123 }
2124 });
2125 } else {
2126 callback.error(
2127 R.string.error_publish_avatar_server_reject,
2128 avatar);
2129 }
2130 }
2131 });
2132 } else {
2133 callback.error(R.string.error_publish_avatar_converting, null);
2134 }
2135 }
2136
2137 public void fetchAvatar(Account account, Avatar avatar) {
2138 fetchAvatar(account, avatar, null);
2139 }
2140
2141 private static String generateFetchKey(Account account, final Avatar avatar) {
2142 return account.getJid().toBareJid()+"_"+avatar.owner+"_"+avatar.sha1sum;
2143 }
2144
2145 public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2146 final String KEY = generateFetchKey(account, avatar);
2147 synchronized(this.mInProgressAvatarFetches) {
2148 if (this.mInProgressAvatarFetches.contains(KEY)) {
2149 return;
2150 } else {
2151 switch (avatar.origin) {
2152 case PEP:
2153 this.mInProgressAvatarFetches.add(KEY);
2154 fetchAvatarPep(account, avatar, callback);
2155 break;
2156 case VCARD:
2157 this.mInProgressAvatarFetches.add(KEY);
2158 fetchAvatarVcard(account, avatar, callback);
2159 break;
2160 }
2161 }
2162 }
2163 }
2164
2165 private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2166 IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar);
2167 sendIqPacket(account, packet, new OnIqPacketReceived() {
2168
2169 @Override
2170 public void onIqPacketReceived(Account account, IqPacket result) {
2171 synchronized (mInProgressAvatarFetches) {
2172 mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
2173 }
2174 final String ERROR = account.getJid().toBareJid()
2175 + ": fetching avatar for " + avatar.owner + " failed ";
2176 if (result.getType() == IqPacket.TYPE.RESULT) {
2177 avatar.image = mIqParser.avatarData(result);
2178 if (avatar.image != null) {
2179 if (getFileBackend().save(avatar)) {
2180 if (account.getJid().toBareJid().equals(avatar.owner)) {
2181 if (account.setAvatar(avatar.getFilename())) {
2182 databaseBackend.updateAccount(account);
2183 }
2184 getAvatarService().clear(account);
2185 updateConversationUi();
2186 updateAccountUi();
2187 } else {
2188 Contact contact = account.getRoster()
2189 .getContact(avatar.owner);
2190 contact.setAvatar(avatar);
2191 getAvatarService().clear(contact);
2192 updateConversationUi();
2193 updateRosterUi();
2194 }
2195 if (callback != null) {
2196 callback.success(avatar);
2197 }
2198 Log.d(Config.LOGTAG, account.getJid().toBareJid()
2199 + ": succesfuly fetched pep avatar for " + avatar.owner);
2200 return;
2201 }
2202 } else {
2203
2204 Log.d(Config.LOGTAG, ERROR + "(parsing error)");
2205 }
2206 } else {
2207 Element error = result.findChild("error");
2208 if (error == null) {
2209 Log.d(Config.LOGTAG, ERROR + "(server error)");
2210 } else {
2211 Log.d(Config.LOGTAG, ERROR + error.toString());
2212 }
2213 }
2214 if (callback != null) {
2215 callback.error(0, null);
2216 }
2217
2218 }
2219 });
2220 }
2221
2222 private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
2223 IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
2224 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2225 @Override
2226 public void onIqPacketReceived(Account account, IqPacket packet) {
2227 synchronized (mInProgressAvatarFetches) {
2228 mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
2229 }
2230 if (packet.getType() == IqPacket.TYPE.RESULT) {
2231 Element vCard = packet.findChild("vCard", "vcard-temp");
2232 Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
2233 String image = photo != null ? photo.findChildContent("BINVAL") : null;
2234 if (image != null) {
2235 avatar.image = image;
2236 if (getFileBackend().save(avatar)) {
2237 Log.d(Config.LOGTAG, account.getJid().toBareJid()
2238 + ": successfully fetched vCard avatar for " + avatar.owner);
2239 Contact contact = account.getRoster()
2240 .getContact(avatar.owner);
2241 contact.setAvatar(avatar);
2242 getAvatarService().clear(contact);
2243 updateConversationUi();
2244 updateRosterUi();
2245 }
2246 }
2247 }
2248 }
2249 });
2250 }
2251
2252 public void checkForAvatar(Account account, final UiCallback<Avatar> callback) {
2253 IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
2254 this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2255
2256 @Override
2257 public void onIqPacketReceived(Account account, IqPacket packet) {
2258 if (packet.getType() == IqPacket.TYPE.RESULT) {
2259 Element pubsub = packet.findChild("pubsub",
2260 "http://jabber.org/protocol/pubsub");
2261 if (pubsub != null) {
2262 Element items = pubsub.findChild("items");
2263 if (items != null) {
2264 Avatar avatar = Avatar.parseMetadata(items);
2265 if (avatar != null) {
2266 avatar.owner = account.getJid().toBareJid();
2267 if (fileBackend.isAvatarCached(avatar)) {
2268 if (account.setAvatar(avatar.getFilename())) {
2269 databaseBackend.updateAccount(account);
2270 }
2271 getAvatarService().clear(account);
2272 callback.success(avatar);
2273 } else {
2274 fetchAvatarPep(account, avatar, callback);
2275 }
2276 return;
2277 }
2278 }
2279 }
2280 }
2281 callback.error(0, null);
2282 }
2283 });
2284 }
2285
2286 public void deleteContactOnServer(Contact contact) {
2287 contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
2288 contact.resetOption(Contact.Options.DIRTY_PUSH);
2289 contact.setOption(Contact.Options.DIRTY_DELETE);
2290 Account account = contact.getAccount();
2291 if (account.getStatus() == Account.State.ONLINE) {
2292 IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2293 Element item = iq.query(Xmlns.ROSTER).addChild("item");
2294 item.setAttribute("jid", contact.getJid().toString());
2295 item.setAttribute("subscription", "remove");
2296 account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler);
2297 }
2298 }
2299
2300 public void updateConversation(Conversation conversation) {
2301 this.databaseBackend.updateConversation(conversation);
2302 }
2303
2304 private void reconnectAccount(final Account account, final boolean force, final boolean interactive) {
2305 synchronized (account) {
2306 if (account.getXmppConnection() != null) {
2307 disconnect(account, force);
2308 }
2309 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
2310
2311 synchronized (this.mInProgressAvatarFetches) {
2312 for(Iterator<String> iterator = this.mInProgressAvatarFetches.iterator(); iterator.hasNext();) {
2313 final String KEY = iterator.next();
2314 if (KEY.startsWith(account.getJid().toBareJid()+"_")) {
2315 iterator.remove();
2316 }
2317 }
2318 }
2319
2320 if (account.getXmppConnection() == null) {
2321 account.setXmppConnection(createConnection(account));
2322 }
2323 Thread thread = new Thread(account.getXmppConnection());
2324 account.getXmppConnection().setInteractive(interactive);
2325 thread.start();
2326 scheduleWakeUpCall(Config.CONNECT_TIMEOUT, account.getUuid().hashCode());
2327 } else {
2328 account.getRoster().clearPresences();
2329 account.setXmppConnection(null);
2330 }
2331 }
2332 }
2333
2334 public void reconnectAccountInBackground(final Account account) {
2335 new Thread(new Runnable() {
2336 @Override
2337 public void run() {
2338 reconnectAccount(account,false,true);
2339 }
2340 }).start();
2341 }
2342
2343 public void invite(Conversation conversation, Jid contact) {
2344 Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+": inviting "+contact+" to "+conversation.getJid().toBareJid());
2345 MessagePacket packet = mMessageGenerator.invite(conversation, contact);
2346 sendMessagePacket(conversation.getAccount(), packet);
2347 }
2348
2349 public void directInvite(Conversation conversation, Jid jid) {
2350 MessagePacket packet = mMessageGenerator.directInvite(conversation, jid);
2351 sendMessagePacket(conversation.getAccount(),packet);
2352 }
2353
2354 public void resetSendingToWaiting(Account account) {
2355 for (Conversation conversation : getConversations()) {
2356 if (conversation.getAccount() == account) {
2357 conversation.findUnsentTextMessages(new Conversation.OnMessageFound() {
2358
2359 @Override
2360 public void onMessageFound(Message message) {
2361 markMessage(message, Message.STATUS_WAITING);
2362 }
2363 });
2364 }
2365 }
2366 }
2367
2368 public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) {
2369 if (uuid == null) {
2370 return null;
2371 }
2372 for (Conversation conversation : getConversations()) {
2373 if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) {
2374 final Message message = conversation.findSentMessageWithUuidOrRemoteId(uuid);
2375 if (message != null) {
2376 markMessage(message, status);
2377 }
2378 return message;
2379 }
2380 }
2381 return null;
2382 }
2383
2384 public boolean markMessage(Conversation conversation, String uuid, int status) {
2385 if (uuid == null) {
2386 return false;
2387 } else {
2388 Message message = conversation.findSentMessageWithUuid(uuid);
2389 if (message != null) {
2390 markMessage(message, status);
2391 return true;
2392 } else {
2393 return false;
2394 }
2395 }
2396 }
2397
2398 public void markMessage(Message message, int status) {
2399 if (status == Message.STATUS_SEND_FAILED
2400 && (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
2401 .getStatus() == Message.STATUS_SEND_DISPLAYED)) {
2402 return;
2403 }
2404 message.setStatus(status);
2405 databaseBackend.updateMessage(message);
2406 updateConversationUi();
2407 }
2408
2409 public SharedPreferences getPreferences() {
2410 return PreferenceManager
2411 .getDefaultSharedPreferences(getApplicationContext());
2412 }
2413
2414 public boolean forceEncryption() {
2415 return getPreferences().getBoolean("force_encryption", false);
2416 }
2417
2418 public boolean confirmMessages() {
2419 return getPreferences().getBoolean("confirm_messages", true);
2420 }
2421
2422 public boolean sendChatStates() {
2423 return getPreferences().getBoolean("chat_states", false);
2424 }
2425
2426 public boolean saveEncryptedMessages() {
2427 return !getPreferences().getBoolean("dont_save_encrypted", false);
2428 }
2429
2430 public boolean indicateReceived() {
2431 return getPreferences().getBoolean("indicate_received", false);
2432 }
2433
2434 public int unreadCount() {
2435 int count = 0;
2436 for(Conversation conversation : getConversations()) {
2437 count += conversation.unreadCount();
2438 }
2439 return count;
2440 }
2441
2442
2443 public void showErrorToastInUi(int resId) {
2444 if (mOnShowErrorToast != null) {
2445 mOnShowErrorToast.onShowErrorToast(resId);
2446 }
2447 }
2448
2449 public void updateConversationUi() {
2450 if (mOnConversationUpdate != null) {
2451 mOnConversationUpdate.onConversationUpdate();
2452 }
2453 }
2454
2455 public void updateAccountUi() {
2456 if (mOnAccountUpdate != null) {
2457 mOnAccountUpdate.onAccountUpdate();
2458 }
2459 }
2460
2461 public void updateRosterUi() {
2462 if (mOnRosterUpdate != null) {
2463 mOnRosterUpdate.onRosterUpdate();
2464 }
2465 }
2466
2467 public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
2468 if (mOnUpdateBlocklist != null) {
2469 mOnUpdateBlocklist.OnUpdateBlocklist(status);
2470 }
2471 }
2472
2473 public void updateMucRosterUi() {
2474 if (mOnMucRosterUpdate != null) {
2475 mOnMucRosterUpdate.onMucRosterUpdate();
2476 }
2477 }
2478
2479 public void keyStatusUpdated() {
2480 if(mOnKeyStatusUpdated != null) {
2481 mOnKeyStatusUpdated.onKeyStatusUpdated();
2482 }
2483 }
2484
2485 public Account findAccountByJid(final Jid accountJid) {
2486 for (Account account : this.accounts) {
2487 if (account.getJid().toBareJid().equals(accountJid.toBareJid())) {
2488 return account;
2489 }
2490 }
2491 return null;
2492 }
2493
2494 public Conversation findConversationByUuid(String uuid) {
2495 for (Conversation conversation : getConversations()) {
2496 if (conversation.getUuid().equals(uuid)) {
2497 return conversation;
2498 }
2499 }
2500 return null;
2501 }
2502
2503 public void markRead(final Conversation conversation) {
2504 mNotificationService.clear(conversation);
2505 conversation.markRead();
2506 updateUnreadCountBadge();
2507 }
2508
2509 public synchronized void updateUnreadCountBadge() {
2510 int count = unreadCount();
2511 if (unreadCount != count) {
2512 Log.d(Config.LOGTAG, "update unread count to " + count);
2513 if (count > 0) {
2514 ShortcutBadger.with(getApplicationContext()).count(count);
2515 } else {
2516 ShortcutBadger.with(getApplicationContext()).remove();
2517 }
2518 unreadCount = count;
2519 }
2520 }
2521
2522 public void sendReadMarker(final Conversation conversation) {
2523 final Message markable = conversation.getLatestMarkableMessage();
2524 this.markRead(conversation);
2525 if (confirmMessages() && markable != null && markable.getRemoteMsgId() != null) {
2526 Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
2527 Account account = conversation.getAccount();
2528 final Jid to = markable.getCounterpart();
2529 MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId());
2530 this.sendMessagePacket(conversation.getAccount(), packet);
2531 }
2532 updateConversationUi();
2533 }
2534
2535 public SecureRandom getRNG() {
2536 return this.mRandom;
2537 }
2538
2539 public MemorizingTrustManager getMemorizingTrustManager() {
2540 return this.mMemorizingTrustManager;
2541 }
2542
2543 public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
2544 this.mMemorizingTrustManager = trustManager;
2545 }
2546
2547 public void updateMemorizingTrustmanager() {
2548 final MemorizingTrustManager tm;
2549 final boolean dontTrustSystemCAs = getPreferences().getBoolean("dont_trust_system_cas", false);
2550 if (dontTrustSystemCAs) {
2551 tm = new MemorizingTrustManager(getApplicationContext(), null);
2552 } else {
2553 tm = new MemorizingTrustManager(getApplicationContext());
2554 }
2555 setMemorizingTrustManager(tm);
2556 }
2557
2558 public PowerManager getPowerManager() {
2559 return this.pm;
2560 }
2561
2562 public LruCache<String, Bitmap> getBitmapCache() {
2563 return this.mBitmapCache;
2564 }
2565
2566 public void syncRosterToDisk(final Account account) {
2567 Runnable runnable = new Runnable() {
2568
2569 @Override
2570 public void run() {
2571 databaseBackend.writeRoster(account.getRoster());
2572 }
2573 };
2574 mDatabaseExecutor.execute(runnable);
2575
2576 }
2577
2578 public List<String> getKnownHosts() {
2579 final List<String> hosts = new ArrayList<>();
2580 for (final Account account : getAccounts()) {
2581 if (!hosts.contains(account.getServer().toString())) {
2582 hosts.add(account.getServer().toString());
2583 }
2584 for (final Contact contact : account.getRoster().getContacts()) {
2585 if (contact.showInRoster()) {
2586 final String server = contact.getServer().toString();
2587 if (server != null && !hosts.contains(server)) {
2588 hosts.add(server);
2589 }
2590 }
2591 }
2592 }
2593 return hosts;
2594 }
2595
2596 public List<String> getKnownConferenceHosts() {
2597 final ArrayList<String> mucServers = new ArrayList<>();
2598 for (final Account account : accounts) {
2599 if (account.getXmppConnection() != null) {
2600 final String server = account.getXmppConnection().getMucServer();
2601 if (server != null && !mucServers.contains(server)) {
2602 mucServers.add(server);
2603 }
2604 }
2605 }
2606 return mucServers;
2607 }
2608
2609 public void sendMessagePacket(Account account, MessagePacket packet) {
2610 XmppConnection connection = account.getXmppConnection();
2611 if (connection != null) {
2612 connection.sendMessagePacket(packet);
2613 }
2614 }
2615
2616 public void sendPresencePacket(Account account, PresencePacket packet) {
2617 XmppConnection connection = account.getXmppConnection();
2618 if (connection != null) {
2619 connection.sendPresencePacket(packet);
2620 }
2621 }
2622
2623 public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
2624 final XmppConnection connection = account.getXmppConnection();
2625 if (connection != null) {
2626 connection.sendIqPacket(packet, callback);
2627 }
2628 }
2629
2630 public void sendPresence(final Account account) {
2631 sendPresencePacket(account, mPresenceGenerator.selfPresence(account, getTargetPresence()));
2632 }
2633
2634 public void refreshAllPresences() {
2635 for (Account account : getAccounts()) {
2636 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
2637 sendPresence(account);
2638 }
2639 }
2640 }
2641
2642 public void sendOfflinePresence(final Account account) {
2643 sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
2644 }
2645
2646 public MessageGenerator getMessageGenerator() {
2647 return this.mMessageGenerator;
2648 }
2649
2650 public PresenceGenerator getPresenceGenerator() {
2651 return this.mPresenceGenerator;
2652 }
2653
2654 public IqGenerator getIqGenerator() {
2655 return this.mIqGenerator;
2656 }
2657
2658 public IqParser getIqParser() {
2659 return this.mIqParser;
2660 }
2661
2662 public JingleConnectionManager getJingleConnectionManager() {
2663 return this.mJingleConnectionManager;
2664 }
2665
2666 public MessageArchiveService getMessageArchiveService() {
2667 return this.mMessageArchiveService;
2668 }
2669
2670 public List<Contact> findContacts(Jid jid) {
2671 ArrayList<Contact> contacts = new ArrayList<>();
2672 for (Account account : getAccounts()) {
2673 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
2674 Contact contact = account.getRoster().getContactFromRoster(jid);
2675 if (contact != null) {
2676 contacts.add(contact);
2677 }
2678 }
2679 }
2680 return contacts;
2681 }
2682
2683 public NotificationService getNotificationService() {
2684 return this.mNotificationService;
2685 }
2686
2687 public HttpConnectionManager getHttpConnectionManager() {
2688 return this.mHttpConnectionManager;
2689 }
2690
2691 public void resendFailedMessages(final Message message) {
2692 final Collection<Message> messages = new ArrayList<>();
2693 Message current = message;
2694 while (current.getStatus() == Message.STATUS_SEND_FAILED) {
2695 messages.add(current);
2696 if (current.mergeable(current.next())) {
2697 current = current.next();
2698 } else {
2699 break;
2700 }
2701 }
2702 for (final Message msg : messages) {
2703 msg.setTime(System.currentTimeMillis());
2704 markMessage(msg, Message.STATUS_WAITING);
2705 this.resendMessage(msg,false);
2706 }
2707 }
2708
2709 public void clearConversationHistory(final Conversation conversation) {
2710 conversation.clearMessages();
2711 conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam
2712 conversation.resetLastMessageTransmitted();
2713 new Thread(new Runnable() {
2714 @Override
2715 public void run() {
2716 databaseBackend.deleteMessagesInConversation(conversation);
2717 }
2718 }).start();
2719 }
2720
2721 public void sendBlockRequest(final Blockable blockable) {
2722 if (blockable != null && blockable.getBlockedJid() != null) {
2723 final Jid jid = blockable.getBlockedJid();
2724 this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid), new OnIqPacketReceived() {
2725
2726 @Override
2727 public void onIqPacketReceived(final Account account, final IqPacket packet) {
2728 if (packet.getType() == IqPacket.TYPE.RESULT) {
2729 account.getBlocklist().add(jid);
2730 updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
2731 }
2732 }
2733 });
2734 }
2735 }
2736
2737 public void sendUnblockRequest(final Blockable blockable) {
2738 if (blockable != null && blockable.getJid() != null) {
2739 final Jid jid = blockable.getBlockedJid();
2740 this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() {
2741 @Override
2742 public void onIqPacketReceived(final Account account, final IqPacket packet) {
2743 if (packet.getType() == IqPacket.TYPE.RESULT) {
2744 account.getBlocklist().remove(jid);
2745 updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
2746 }
2747 }
2748 });
2749 }
2750 }
2751
2752 public interface OnAccountCreated {
2753 void onAccountCreated(Account account);
2754 void informUser(int r);
2755 }
2756
2757 public interface OnMoreMessagesLoaded {
2758 void onMoreMessagesLoaded(int count, Conversation conversation);
2759
2760 void informUser(int r);
2761 }
2762
2763 public interface OnAccountPasswordChanged {
2764 void onPasswordChangeSucceeded();
2765
2766 void onPasswordChangeFailed();
2767 }
2768
2769 public interface OnAffiliationChanged {
2770 void onAffiliationChangedSuccessful(Jid jid);
2771
2772 void onAffiliationChangeFailed(Jid jid, int resId);
2773 }
2774
2775 public interface OnRoleChanged {
2776 void onRoleChangedSuccessful(String nick);
2777
2778 void onRoleChangeFailed(String nick, int resid);
2779 }
2780
2781 public interface OnConversationUpdate {
2782 void onConversationUpdate();
2783 }
2784
2785 public interface OnAccountUpdate {
2786 void onAccountUpdate();
2787 }
2788
2789 public interface OnRosterUpdate {
2790 void onRosterUpdate();
2791 }
2792
2793 public interface OnMucRosterUpdate {
2794 void onMucRosterUpdate();
2795 }
2796
2797 public interface OnConferenceConfigurationFetched {
2798 void onConferenceConfigurationFetched(Conversation conversation);
2799 }
2800
2801 public interface OnConferenceOptionsPushed {
2802 void onPushSucceeded();
2803
2804 void onPushFailed();
2805 }
2806
2807 public interface OnShowErrorToast {
2808 void onShowErrorToast(int resId);
2809 }
2810
2811 public class XmppConnectionBinder extends Binder {
2812 public XmppConnectionService getService() {
2813 return XmppConnectionService.this;
2814 }
2815 }
2816}