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