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