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