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