XmppConnectionService.java

   1package eu.siacs.conversations.services;
   2
   3import java.security.SecureRandom;
   4import java.text.SimpleDateFormat;
   5import java.util.ArrayList;
   6import java.util.Collections;
   7import java.util.Comparator;
   8import java.util.Date;
   9import java.util.Hashtable;
  10import java.util.List;
  11import java.util.Locale;
  12import java.util.TimeZone;
  13import java.util.concurrent.CopyOnWriteArrayList;
  14
  15import org.openintents.openpgp.util.OpenPgpApi;
  16import org.openintents.openpgp.util.OpenPgpServiceConnection;
  17
  18import de.duenndns.ssl.MemorizingTrustManager;
  19
  20import net.java.otr4j.OtrException;
  21import net.java.otr4j.session.Session;
  22import net.java.otr4j.session.SessionID;
  23import net.java.otr4j.session.SessionStatus;
  24import eu.siacs.conversations.Config;
  25import eu.siacs.conversations.R;
  26import eu.siacs.conversations.crypto.PgpEngine;
  27import eu.siacs.conversations.entities.Account;
  28import eu.siacs.conversations.entities.Bookmark;
  29import eu.siacs.conversations.entities.Contact;
  30import eu.siacs.conversations.entities.Conversation;
  31import eu.siacs.conversations.entities.Downloadable;
  32import eu.siacs.conversations.entities.Message;
  33import eu.siacs.conversations.entities.MucOptions;
  34import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
  35import eu.siacs.conversations.entities.Presences;
  36import eu.siacs.conversations.generator.IqGenerator;
  37import eu.siacs.conversations.generator.MessageGenerator;
  38import eu.siacs.conversations.generator.PresenceGenerator;
  39import eu.siacs.conversations.http.HttpConnectionManager;
  40import eu.siacs.conversations.parser.IqParser;
  41import eu.siacs.conversations.parser.MessageParser;
  42import eu.siacs.conversations.parser.PresenceParser;
  43import eu.siacs.conversations.persistance.DatabaseBackend;
  44import eu.siacs.conversations.persistance.FileBackend;
  45import eu.siacs.conversations.ui.UiCallback;
  46import eu.siacs.conversations.utils.CryptoHelper;
  47import eu.siacs.conversations.utils.ExceptionHelper;
  48import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
  49import eu.siacs.conversations.utils.PRNGFixes;
  50import eu.siacs.conversations.utils.PhoneHelper;
  51import eu.siacs.conversations.utils.UIHelper;
  52import eu.siacs.conversations.xml.Element;
  53import eu.siacs.conversations.xmpp.OnBindListener;
  54import eu.siacs.conversations.xmpp.OnContactStatusChanged;
  55import eu.siacs.conversations.xmpp.OnIqPacketReceived;
  56import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
  57import eu.siacs.conversations.xmpp.OnStatusChanged;
  58import eu.siacs.conversations.xmpp.XmppConnection;
  59import eu.siacs.conversations.xmpp.jid.InvalidJidException;
  60import eu.siacs.conversations.xmpp.jid.Jid;
  61import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
  62import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
  63import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
  64import eu.siacs.conversations.xmpp.pep.Avatar;
  65import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  66import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
  67import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
  68import android.annotation.SuppressLint;
  69import android.app.AlarmManager;
  70import android.app.PendingIntent;
  71import android.app.Service;
  72import android.content.Context;
  73import android.content.Intent;
  74import android.content.SharedPreferences;
  75import android.database.ContentObserver;
  76import android.graphics.Bitmap;
  77import android.net.ConnectivityManager;
  78import android.net.NetworkInfo;
  79import android.net.Uri;
  80import android.os.Binder;
  81import android.os.Bundle;
  82import android.os.FileObserver;
  83import android.os.IBinder;
  84import android.os.PowerManager;
  85import android.os.PowerManager.WakeLock;
  86import android.os.SystemClock;
  87import android.preference.PreferenceManager;
  88import android.provider.ContactsContract;
  89import android.util.Log;
  90import android.util.LruCache;
  91
  92public class XmppConnectionService extends Service {
  93
  94	public DatabaseBackend databaseBackend;
  95	private FileBackend fileBackend = new FileBackend(this);
  96
  97	private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
  98	public static String ACTION_CLEAR_NOTIFICATION = "clear_notification";
  99
 100	private MemorizingTrustManager mMemorizingTrustManager;
 101
 102	private NotificationService mNotificationService = new NotificationService(
 103			this);
 104
 105	private MessageParser mMessageParser = new MessageParser(this);
 106	private PresenceParser mPresenceParser = new PresenceParser(this);
 107	private IqParser mIqParser = new IqParser(this);
 108	private MessageGenerator mMessageGenerator = new MessageGenerator(this);
 109	private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
 110
 111	private List<Account> accounts;
 112	private CopyOnWriteArrayList<Conversation> conversations = null;
 113	private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
 114			this);
 115	private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
 116			this);
 117	private AvatarService mAvatarService = new AvatarService(this);
 118
 119	private OnConversationUpdate mOnConversationUpdate = null;
 120	private Integer convChangedListenerCount = 0;
 121	private OnAccountUpdate mOnAccountUpdate = null;
 122	private Integer accountChangedListenerCount = 0;
 123	private OnRosterUpdate mOnRosterUpdate = null;
 124	private Integer rosterChangedListenerCount = 0;
 125	public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
 126
 127		@Override
 128		public void onContactStatusChanged(Contact contact, boolean online) {
 129			Conversation conversation = find(getConversations(), contact);
 130			if (conversation != null) {
 131				if (online && contact.getPresences().size() > 1) {
 132					conversation.endOtrIfNeeded();
 133				} else {
 134					conversation.resetOtrSession();
 135				}
 136				if (online && (contact.getPresences().size() == 1)) {
 137					sendUnsendMessages(conversation);
 138				}
 139			}
 140		}
 141	};
 142
 143	private SecureRandom mRandom;
 144
 145	private ContentObserver contactObserver = new ContentObserver(null) {
 146		@Override
 147		public void onChange(boolean selfChange) {
 148			super.onChange(selfChange);
 149			Intent intent = new Intent(getApplicationContext(),
 150					XmppConnectionService.class);
 151			intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
 152			startService(intent);
 153		}
 154	};
 155
 156	private FileObserver fileObserver = new FileObserver(
 157			FileBackend.getConversationsDirectory()) {
 158
 159		@Override
 160		public void onEvent(int event, String path) {
 161			if (event == FileObserver.DELETE) {
 162				markFileDeleted(path.split("\\.")[0]);
 163			}
 164		}
 165	};
 166
 167	private final IBinder mBinder = new XmppConnectionBinder();
 168	private OnStatusChanged statusListener = new OnStatusChanged() {
 169
 170		@Override
 171		public void onStatusChanged(Account account) {
 172			XmppConnection connection = account.getXmppConnection();
 173			if (mOnAccountUpdate != null) {
 174				mOnAccountUpdate.onAccountUpdate();
 175			}
 176			if (account.getStatus() == Account.STATUS_ONLINE) {
 177				for (Conversation conversation : account.pendingConferenceLeaves) {
 178					leaveMuc(conversation);
 179				}
 180				for (Conversation conversation : account.pendingConferenceJoins) {
 181					joinMuc(conversation);
 182				}
 183				mJingleConnectionManager.cancelInTransmission();
 184				List<Conversation> conversations = getConversations();
 185                for (Conversation conversation : conversations) {
 186                    if (conversation.getAccount() == account) {
 187                        conversation.startOtrIfNeeded();
 188                        sendUnsendMessages(conversation);
 189                    }
 190                }
 191				if (connection != null && connection.getFeatures().csi()) {
 192					if (checkListeners()) {
 193						Log.d(Config.LOGTAG, account.getJid().toBareJid()
 194								+ " sending csi//inactive");
 195						connection.sendInactive();
 196					} else {
 197						Log.d(Config.LOGTAG, account.getJid().toBareJid()
 198								+ " sending csi//active");
 199						connection.sendActive();
 200					}
 201				}
 202				syncDirtyContacts(account);
 203				scheduleWakeupCall(Config.PING_MAX_INTERVAL, true);
 204			} else if (account.getStatus() == Account.STATUS_OFFLINE) {
 205				resetSendingToWaiting(account);
 206				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 207					int timeToReconnect = mRandom.nextInt(50) + 10;
 208					scheduleWakeupCall(timeToReconnect, false);
 209				}
 210			} else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) {
 211				databaseBackend.updateAccount(account);
 212				reconnectAccount(account, true);
 213			} else if ((account.getStatus() != Account.STATUS_CONNECTING)
 214					&& (account.getStatus() != Account.STATUS_NO_INTERNET)) {
 215				if (connection != null) {
 216					int next = connection.getTimeToNextAttempt();
 217					Log.d(Config.LOGTAG, account.getJid().toBareJid()
 218							+ ": error connecting account. try again in "
 219							+ next + "s for the "
 220							+ (connection.getAttempt() + 1) + " time");
 221					scheduleWakeupCall((int) (next * 1.2), false);
 222				}
 223			}
 224			UIHelper.showErrorNotification(getApplicationContext(),
 225					getAccounts());
 226		}
 227	};
 228
 229	private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
 230
 231		@Override
 232		public void onJinglePacketReceived(Account account, JinglePacket packet) {
 233			mJingleConnectionManager.deliverPacket(account, packet);
 234		}
 235	};
 236
 237	private OpenPgpServiceConnection pgpServiceConnection;
 238	private PgpEngine mPgpEngine = null;
 239	private Intent pingIntent;
 240	private PendingIntent pendingPingIntent = null;
 241	private WakeLock wakeLock;
 242	private PowerManager pm;
 243	private OnBindListener mOnBindListener = new OnBindListener() {
 244
 245		@Override
 246		public void onBind(final Account account) {
 247			account.getRoster().clearPresences();
 248			account.clearPresences(); // self presences
 249			account.pendingConferenceJoins.clear();
 250			account.pendingConferenceLeaves.clear();
 251			fetchRosterFromServer(account);
 252			fetchBookmarks(account);
 253			sendPresencePacket(account,
 254					mPresenceGenerator.sendPresence(account));
 255			connectMultiModeConversations(account);
 256			updateConversationUi();
 257		}
 258	};
 259
 260	private OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
 261
 262		@Override
 263		public void onMessageAcknowledged(Account account, String uuid) {
 264			for (Conversation conversation : getConversations()) {
 265				if (conversation.getAccount() == account) {
 266					for (Message message : conversation.getMessages()) {
 267						if ((message.getStatus() == Message.STATUS_UNSEND || message
 268								.getStatus() == Message.STATUS_WAITING)
 269								&& message.getUuid().equals(uuid)) {
 270							markMessage(message, Message.STATUS_SEND);
 271							return;
 272						}
 273					}
 274				}
 275			}
 276		}
 277	};
 278	private LruCache<String, Bitmap> mBitmapCache;
 279
 280	public PgpEngine getPgpEngine() {
 281		if (pgpServiceConnection.isBound()) {
 282			if (this.mPgpEngine == null) {
 283				this.mPgpEngine = new PgpEngine(new OpenPgpApi(
 284						getApplicationContext(),
 285						pgpServiceConnection.getService()), this);
 286			}
 287			return mPgpEngine;
 288		} else {
 289			return null;
 290		}
 291
 292	}
 293
 294	public FileBackend getFileBackend() {
 295		return this.fileBackend;
 296	}
 297
 298	public AvatarService getAvatarService() {
 299		return this.mAvatarService;
 300	}
 301
 302	public Message attachImageToConversation(final Conversation conversation,
 303			final Uri uri, final UiCallback<Message> callback) {
 304		final Message message;
 305		if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
 306			message = new Message(conversation, "",
 307					Message.ENCRYPTION_DECRYPTED);
 308		} else {
 309			message = new Message(conversation, "",
 310					conversation.getNextEncryption(forceEncryption()));
 311		}
 312		message.setCounterpart(conversation.getNextCounterpart());
 313		message.setType(Message.TYPE_IMAGE);
 314		message.setStatus(Message.STATUS_OFFERED);
 315		new Thread(new Runnable() {
 316
 317			@Override
 318			public void run() {
 319				try {
 320					getFileBackend().copyImageToPrivateStorage(message, uri);
 321					if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
 322						getPgpEngine().encrypt(message, callback);
 323					} else {
 324						callback.success(message);
 325					}
 326				} catch (FileBackend.ImageCopyException e) {
 327					callback.error(e.getResId(), message);
 328				}
 329			}
 330		}).start();
 331		return message;
 332	}
 333
 334	public Conversation find(Bookmark bookmark) {
 335		return find(bookmark.getAccount(), bookmark.getJid());
 336	}
 337
 338	public Conversation find(final Account account, final Jid jid) {
 339		return find(getConversations(), account, jid);
 340	}
 341
 342	public class XmppConnectionBinder extends Binder {
 343		public XmppConnectionService getService() {
 344			return XmppConnectionService.this;
 345		}
 346	}
 347
 348	@Override
 349	public int onStartCommand(Intent intent, int flags, int startId) {
 350		if (intent != null && intent.getAction() != null) {
 351			if (intent.getAction().equals(ACTION_MERGE_PHONE_CONTACTS)) {
 352				mergePhoneContactsWithRoster();
 353				return START_STICKY;
 354			} else if (intent.getAction().equals(Intent.ACTION_SHUTDOWN)) {
 355				logoutAndSave();
 356				return START_NOT_STICKY;
 357			} else if (intent.getAction().equals(ACTION_CLEAR_NOTIFICATION)) {
 358				mNotificationService.clear();
 359			}
 360		}
 361		this.wakeLock.acquire();
 362
 363		for (Account account : accounts) {
 364			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 365				if (!hasInternetConnection()) {
 366					account.setStatus(Account.STATUS_NO_INTERNET);
 367					if (statusListener != null) {
 368						statusListener.onStatusChanged(account);
 369					}
 370				} else {
 371					if (account.getStatus() == Account.STATUS_NO_INTERNET) {
 372						account.setStatus(Account.STATUS_OFFLINE);
 373						if (statusListener != null) {
 374							statusListener.onStatusChanged(account);
 375						}
 376					}
 377					if (account.getStatus() == Account.STATUS_ONLINE) {
 378						long lastReceived = account.getXmppConnection()
 379								.getLastPacketReceived();
 380						long lastSent = account.getXmppConnection()
 381								.getLastPingSent();
 382						if (lastSent - lastReceived >= Config.PING_TIMEOUT * 1000) {
 383							Log.d(Config.LOGTAG, account.getJid()
 384									+ ": ping timeout");
 385							this.reconnectAccount(account, true);
 386						} else if (SystemClock.elapsedRealtime() - lastReceived >= Config.PING_MIN_INTERVAL * 1000) {
 387							account.getXmppConnection().sendPing();
 388							this.scheduleWakeupCall(2, false);
 389						}
 390					} else if (account.getStatus() == Account.STATUS_OFFLINE) {
 391						if (account.getXmppConnection() == null) {
 392							account.setXmppConnection(this
 393									.createConnection(account));
 394						}
 395						new Thread(account.getXmppConnection()).start();
 396					} else if ((account.getStatus() == Account.STATUS_CONNECTING)
 397							&& ((SystemClock.elapsedRealtime() - account
 398									.getXmppConnection().getLastConnect()) / 1000 >= Config.CONNECT_TIMEOUT)) {
 399						Log.d(Config.LOGTAG, account.getJid()
 400								+ ": time out during connect reconnecting");
 401						reconnectAccount(account, true);
 402					} else {
 403						if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
 404							reconnectAccount(account, true);
 405						}
 406					}
 407					// in any case. reschedule wakup call
 408					this.scheduleWakeupCall(Config.PING_MAX_INTERVAL, true);
 409				}
 410				if (mOnAccountUpdate != null) {
 411					mOnAccountUpdate.onAccountUpdate();
 412				}
 413			}
 414		}
 415		if (wakeLock.isHeld()) {
 416			try {
 417				wakeLock.release();
 418			} catch (final RuntimeException ignored) {
 419			}
 420		}
 421		return START_STICKY;
 422	}
 423
 424	public boolean hasInternetConnection() {
 425		ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
 426				.getSystemService(Context.CONNECTIVITY_SERVICE);
 427		NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
 428		return activeNetwork != null && activeNetwork.isConnected();
 429	}
 430
 431	@SuppressLint("TrulyRandom")
 432	@Override
 433	public void onCreate() {
 434		ExceptionHelper.init(getApplicationContext());
 435		PRNGFixes.apply();
 436		this.mRandom = new SecureRandom();
 437		this.mMemorizingTrustManager = new MemorizingTrustManager(
 438				getApplicationContext());
 439
 440		int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
 441		int cacheSize = maxMemory / 8;
 442		this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
 443			@Override
 444			protected int sizeOf(String key, Bitmap bitmap) {
 445				return bitmap.getByteCount() / 1024;
 446			}
 447		};
 448
 449		this.databaseBackend = DatabaseBackend
 450				.getInstance(getApplicationContext());
 451		this.accounts = databaseBackend.getAccounts();
 452
 453		for (Account account : this.accounts) {
 454			this.databaseBackend.readRoster(account.getRoster());
 455		}
 456		this.mergePhoneContactsWithRoster();
 457		this.getConversations();
 458
 459		getContentResolver().registerContentObserver(
 460				ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
 461		this.fileObserver.startWatching();
 462		this.pgpServiceConnection = new OpenPgpServiceConnection(
 463				getApplicationContext(), "org.sufficientlysecure.keychain");
 464		this.pgpServiceConnection.bindToService();
 465
 466		this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 467		this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
 468				"XmppConnectionService");
 469	}
 470
 471	@Override
 472	public void onDestroy() {
 473		super.onDestroy();
 474		this.logoutAndSave();
 475	}
 476
 477	@Override
 478	public void onTaskRemoved(Intent rootIntent) {
 479		super.onTaskRemoved(rootIntent);
 480		this.logoutAndSave();
 481	}
 482
 483	private void logoutAndSave() {
 484		for (Account account : accounts) {
 485			databaseBackend.writeRoster(account.getRoster());
 486			if (account.getXmppConnection() != null) {
 487				disconnect(account, false);
 488			}
 489		}
 490		Context context = getApplicationContext();
 491		AlarmManager alarmManager = (AlarmManager) context
 492				.getSystemService(Context.ALARM_SERVICE);
 493		Intent intent = new Intent(context, EventReceiver.class);
 494		alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
 495		Log.d(Config.LOGTAG, "good bye");
 496		stopSelf();
 497	}
 498
 499	protected void scheduleWakeupCall(int seconds, boolean ping) {
 500		long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000;
 501		Context context = getApplicationContext();
 502		AlarmManager alarmManager = (AlarmManager) context
 503				.getSystemService(Context.ALARM_SERVICE);
 504
 505		if (ping) {
 506			if (this.pingIntent == null) {
 507				this.pingIntent = new Intent(context, EventReceiver.class);
 508				this.pingIntent.setAction("ping");
 509				this.pingIntent.putExtra("time", timeToWake);
 510				this.pendingPingIntent = PendingIntent.getBroadcast(context, 0,
 511						this.pingIntent, 0);
 512				alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
 513						timeToWake, pendingPingIntent);
 514			} else {
 515				long scheduledTime = this.pingIntent.getLongExtra("time", 0);
 516				if (scheduledTime < SystemClock.elapsedRealtime()
 517						|| (scheduledTime > timeToWake)) {
 518					this.pingIntent.putExtra("time", timeToWake);
 519					alarmManager.cancel(this.pendingPingIntent);
 520					this.pendingPingIntent = PendingIntent.getBroadcast(
 521							context, 0, this.pingIntent, 0);
 522					alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
 523							timeToWake, pendingPingIntent);
 524				}
 525			}
 526		} else {
 527			Intent intent = new Intent(context, EventReceiver.class);
 528			intent.setAction("ping_check");
 529			PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0,
 530					intent, 0);
 531			alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake,
 532					alarmIntent);
 533		}
 534
 535	}
 536
 537	public XmppConnection createConnection(Account account) {
 538		SharedPreferences sharedPref = getPreferences();
 539        account.setResource(sharedPref.getString("resource", "mobile")
 540                .toLowerCase(Locale.getDefault()));
 541        XmppConnection connection = new XmppConnection(account, this);
 542		connection.setOnMessagePacketReceivedListener(this.mMessageParser);
 543		connection.setOnStatusChangedListener(this.statusListener);
 544		connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
 545		connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
 546		connection.setOnJinglePacketReceivedListener(this.jingleListener);
 547		connection.setOnBindListener(this.mOnBindListener);
 548		connection
 549				.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
 550		return connection;
 551	}
 552
 553	public void sendMessage(Message message) {
 554		Account account = message.getConversation().getAccount();
 555		account.deactivateGracePeriod();
 556		Conversation conv = message.getConversation();
 557		MessagePacket packet = null;
 558		boolean saveInDb = true;
 559		boolean send = false;
 560		if (account.getStatus() == Account.STATUS_ONLINE
 561				&& account.getXmppConnection() != null) {
 562			if (message.getType() == Message.TYPE_IMAGE) {
 563				if (message.getCounterpart() != null) {
 564					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 565						if (!conv.hasValidOtrSession()) {
 566							conv.startOtrSession(this, message.getCounterpart().getResourcepart(),
 567									true);
 568							message.setStatus(Message.STATUS_WAITING);
 569						} else if (conv.hasValidOtrSession()
 570								&& conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 571							mJingleConnectionManager
 572									.createNewConnection(message);
 573						}
 574					} else {
 575						mJingleConnectionManager.createNewConnection(message);
 576					}
 577				} else {
 578					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 579						conv.startOtrIfNeeded();
 580					}
 581					message.setStatus(Message.STATUS_WAITING);
 582				}
 583			} else {
 584				if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 585					if (!conv.hasValidOtrSession()&& (message.getCounterpart() != null)) {
 586						conv.startOtrSession(this, message.getCounterpart().getResourcepart(), true);
 587						message.setStatus(Message.STATUS_WAITING);
 588					} else if (conv.hasValidOtrSession()) {
 589						SessionID id = conv.getOtrSession().getSessionID();
 590						try {
 591							message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
 592						} catch (final InvalidJidException e) {
 593							message.setCounterpart(null);
 594						}
 595						if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 596							packet = mMessageGenerator.generateOtrChat(message);
 597							send = true;
 598						} else {
 599							message.setStatus(Message.STATUS_WAITING);
 600							conv.startOtrIfNeeded();
 601						}
 602					} else {
 603						message.setStatus(Message.STATUS_WAITING);
 604					}
 605				} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 606					message.getConversation().endOtrIfNeeded();
 607					failWaitingOtrMessages(message.getConversation());
 608					packet = mMessageGenerator.generatePgpChat(message);
 609					send = true;
 610				} else {
 611					message.getConversation().endOtrIfNeeded();
 612					failWaitingOtrMessages(message.getConversation());
 613					packet = mMessageGenerator.generateChat(message);
 614					send = true;
 615				}
 616			}
 617			if (!account.getXmppConnection().getFeatures().sm()
 618					&& conv.getMode() != Conversation.MODE_MULTI) {
 619				message.setStatus(Message.STATUS_SEND);
 620			}
 621		} else {
 622			message.setStatus(Message.STATUS_WAITING);
 623			if (message.getType() == Message.TYPE_TEXT) {
 624				if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 625					String pgpBody = message.getEncryptedBody();
 626					String decryptedBody = message.getBody();
 627					message.setBody(pgpBody);
 628					message.setEncryption(Message.ENCRYPTION_PGP);
 629					databaseBackend.createMessage(message);
 630					saveInDb = false;
 631					message.setBody(decryptedBody);
 632					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 633				} else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 634					if (conv.hasValidOtrSession()) {
 635						SessionID id = conv.getOtrSession().getSessionID();
 636						try {
 637							message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
 638						} catch (final InvalidJidException e) {
 639							message.setCounterpart(null);
 640						}
 641					} else if (!conv.hasValidOtrSession()
 642							&& message.getCounterpart() != null) {
 643						conv.startOtrSession(this, message.getCounterpart().getResourcepart(), false);
 644					}
 645				}
 646			}
 647
 648		}
 649		conv.add(message);
 650		if (saveInDb) {
 651			if (message.getEncryption() == Message.ENCRYPTION_NONE
 652					|| saveEncryptedMessages()) {
 653				databaseBackend.createMessage(message);
 654			}
 655		}
 656		if ((send) && (packet != null)) {
 657			sendMessagePacket(account, packet);
 658		}
 659		updateConversationUi();
 660	}
 661
 662	private void sendUnsendMessages(Conversation conversation) {
 663		for (int i = 0; i < conversation.getMessages().size(); ++i) {
 664			int status = conversation.getMessages().get(i).getStatus();
 665			if (status == Message.STATUS_WAITING) {
 666				resendMessage(conversation.getMessages().get(i));
 667			}
 668		}
 669	}
 670
 671	private void resendMessage(Message message) {
 672		Account account = message.getConversation().getAccount();
 673		MessagePacket packet = null;
 674		if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 675			Presences presences = message.getConversation().getContact()
 676					.getPresences();
 677			if (!message.getConversation().hasValidOtrSession()) {
 678				if ((message.getCounterpart() != null)
 679						&& (presences.has(message.getCounterpart().getResourcepart()))) {
 680					message.getConversation().startOtrSession(this,
 681							message.getCounterpart().getResourcepart(), true);
 682				} else {
 683					if (presences.size() == 1) {
 684						String presence = presences.asStringArray()[0];
 685						message.getConversation().startOtrSession(this,
 686								presence, true);
 687					}
 688				}
 689			} else {
 690				if (message.getConversation().getOtrSession()
 691						.getSessionStatus() == SessionStatus.ENCRYPTED) {
 692					if (message.getType() == Message.TYPE_TEXT) {
 693						packet = mMessageGenerator.generateOtrChat(message,
 694								true);
 695					} else if (message.getType() == Message.TYPE_IMAGE) {
 696						mJingleConnectionManager.createNewConnection(message);
 697					}
 698				}
 699			}
 700		} else if (message.getType() == Message.TYPE_TEXT) {
 701			if (message.getEncryption() == Message.ENCRYPTION_NONE) {
 702				packet = mMessageGenerator.generateChat(message, true);
 703			} else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED)
 704					|| (message.getEncryption() == Message.ENCRYPTION_PGP)) {
 705				packet = mMessageGenerator.generatePgpChat(message, true);
 706			}
 707		} else if (message.getType() == Message.TYPE_IMAGE) {
 708			Contact contact = message.getConversation().getContact();
 709			Presences presences = contact.getPresences();
 710			if ((message.getCounterpart() != null)
 711					&& (presences.has(message.getCounterpart().getResourcepart()))) {
 712				markMessage(message, Message.STATUS_OFFERED);
 713				mJingleConnectionManager.createNewConnection(message);
 714			} else {
 715				if (presences.size() == 1) {
 716					String presence = presences.asStringArray()[0];
 717					try {
 718						message.setCounterpart(Jid.fromParts(contact.getJid().getLocalpart(), contact.getJid().getDomainpart(), presence));
 719					} catch (InvalidJidException e) {
 720						return;
 721					}
 722					markMessage(message, Message.STATUS_OFFERED);
 723					mJingleConnectionManager.createNewConnection(message);
 724				}
 725			}
 726		}
 727		if (packet != null) {
 728			if (!account.getXmppConnection().getFeatures().sm()
 729					&& message.getConversation().getMode() != Conversation.MODE_MULTI) {
 730				markMessage(message, Message.STATUS_SEND);
 731			} else {
 732				markMessage(message, Message.STATUS_UNSEND);
 733			}
 734			sendMessagePacket(account, packet);
 735		}
 736	}
 737
 738	public void fetchRosterFromServer(Account account) {
 739		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
 740		if (!"".equals(account.getRosterVersion())) {
 741			Log.d(Config.LOGTAG, account.getJid().toBareJid()
 742					+ ": fetching roster version " + account.getRosterVersion());
 743		} else {
 744			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
 745		}
 746		iqPacket.query("jabber:iq:roster").setAttribute("ver",
 747				account.getRosterVersion());
 748		account.getXmppConnection().sendIqPacket(iqPacket,
 749				new OnIqPacketReceived() {
 750
 751					@Override
 752					public void onIqPacketReceived(final Account account,
 753							IqPacket packet) {
 754						Element query = packet.findChild("query");
 755						if (query != null) {
 756							account.getRoster().markAllAsNotInRoster();
 757							mIqParser.rosterItems(account, query);
 758						}
 759					}
 760				});
 761	}
 762
 763	public void fetchBookmarks(Account account) {
 764		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
 765		Element query = iqPacket.query("jabber:iq:private");
 766		query.addChild("storage", "storage:bookmarks");
 767		OnIqPacketReceived callback = new OnIqPacketReceived() {
 768
 769			@Override
 770			public void onIqPacketReceived(Account account, IqPacket packet) {
 771				Element query = packet.query();
 772				List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
 773				Element storage = query.findChild("storage",
 774						"storage:bookmarks");
 775				if (storage != null) {
 776					for (Element item : storage.getChildren()) {
 777						if (item.getName().equals("conference")) {
 778							Bookmark bookmark = Bookmark.parse(item, account);
 779							bookmarks.add(bookmark);
 780							Conversation conversation = find(bookmark);
 781							if (conversation != null) {
 782								conversation.setBookmark(bookmark);
 783							} else {
 784								if (bookmark.autojoin()) {
 785									conversation = findOrCreateConversation(
 786											account, bookmark.getJid(), true);
 787									conversation.setBookmark(bookmark);
 788									joinMuc(conversation);
 789								}
 790							}
 791						}
 792					}
 793				}
 794				account.setBookmarks(bookmarks);
 795			}
 796		};
 797		sendIqPacket(account, iqPacket, callback);
 798
 799	}
 800
 801	public void pushBookmarks(Account account) {
 802		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_SET);
 803		Element query = iqPacket.query("jabber:iq:private");
 804		Element storage = query.addChild("storage", "storage:bookmarks");
 805		for (Bookmark bookmark : account.getBookmarks()) {
 806			storage.addChild(bookmark);
 807		}
 808		sendIqPacket(account, iqPacket, null);
 809	}
 810
 811	private void mergePhoneContactsWithRoster() {
 812		PhoneHelper.loadPhoneContacts(getApplicationContext(),
 813				new OnPhoneContactsLoadedListener() {
 814					@Override
 815					public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
 816						for (Account account : accounts) {
 817							account.getRoster().clearSystemAccounts();
 818						}
 819						for (Bundle phoneContact : phoneContacts) {
 820							for (Account account : accounts) {
 821                                Jid jid;
 822                                try {
 823                                    jid = Jid.fromString(phoneContact.getString("jid"));
 824                                } catch (final InvalidJidException e) {
 825                                    // TODO: Warn if contact import fails here?
 826                                    break;
 827                                }
 828                                final Contact contact = account.getRoster()
 829										.getContact(jid);
 830								String systemAccount = phoneContact
 831										.getInt("phoneid")
 832										+ "#"
 833										+ phoneContact.getString("lookup");
 834								contact.setSystemAccount(systemAccount);
 835								contact.setPhotoUri(phoneContact
 836										.getString("photouri"));
 837								contact.setSystemName(phoneContact
 838										.getString("displayname"));
 839								getAvatarService().clear(contact);
 840							}
 841						}
 842					}
 843				});
 844	}
 845
 846	public List<Conversation> getConversations() {
 847		if (this.conversations == null) {
 848			Hashtable<String, Account> accountLookupTable = new Hashtable<>();
 849			for (Account account : this.accounts) {
 850				accountLookupTable.put(account.getUuid(), account);
 851			}
 852			this.conversations = databaseBackend
 853					.getConversations(Conversation.STATUS_AVAILABLE);
 854			for (Conversation conv : this.conversations) {
 855				Account account = accountLookupTable.get(conv.getAccountUuid());
 856				conv.setAccount(account);
 857				conv.setMessages(databaseBackend.getMessages(conv, 50));
 858				checkDeletedFiles(conv);
 859			}
 860		}
 861		return this.conversations;
 862	}
 863
 864	private void checkDeletedFiles(Conversation conversation) {
 865		for (Message message : conversation.getMessages()) {
 866			if (message.getType() == Message.TYPE_IMAGE
 867					&& message.getEncryption() != Message.ENCRYPTION_PGP) {
 868				if (!getFileBackend().isFileAvailable(message)) {
 869					message.setDownloadable(new DeletedDownloadable());
 870				}
 871			}
 872		}
 873	}
 874
 875	private void markFileDeleted(String uuid) {
 876		for (Conversation conversation : getConversations()) {
 877			for (Message message : conversation.getMessages()) {
 878				if (message.getType() == Message.TYPE_IMAGE
 879						&& message.getEncryption() != Message.ENCRYPTION_PGP
 880						&& message.getUuid().equals(uuid)) {
 881					if (!getFileBackend().isFileAvailable(message)) {
 882						message.setDownloadable(new DeletedDownloadable());
 883						updateConversationUi();
 884					}
 885					return;
 886				}
 887			}
 888		}
 889	}
 890
 891	public void populateWithOrderedConversations(List<Conversation> list) {
 892		populateWithOrderedConversations(list, true);
 893	}
 894
 895	public void populateWithOrderedConversations(List<Conversation> list,
 896			boolean includeConferences) {
 897		list.clear();
 898		if (includeConferences) {
 899			list.addAll(getConversations());
 900		} else {
 901			for (Conversation conversation : getConversations()) {
 902				if (conversation.getMode() == Conversation.MODE_SINGLE) {
 903					list.add(conversation);
 904				}
 905			}
 906		}
 907		Collections.sort(list, new Comparator<Conversation>() {
 908			@Override
 909			public int compare(Conversation lhs, Conversation rhs) {
 910				Message left = lhs.getLatestMessage();
 911				Message right = rhs.getLatestMessage();
 912				if (left.getTimeSent() > right.getTimeSent()) {
 913					return -1;
 914				} else if (left.getTimeSent() < right.getTimeSent()) {
 915					return 1;
 916				} else {
 917					return 0;
 918				}
 919			}
 920		});
 921	}
 922
 923	public int loadMoreMessages(Conversation conversation, long timestamp) {
 924		List<Message> messages = databaseBackend.getMessages(conversation, 50,
 925				timestamp);
 926		for (Message message : messages) {
 927			message.setConversation(conversation);
 928		}
 929		conversation.addAll(0, messages);
 930		return messages.size();
 931	}
 932
 933	public List<Account> getAccounts() {
 934		return this.accounts;
 935	}
 936
 937	public Conversation find(List<Conversation> haystack, Contact contact) {
 938		for (Conversation conversation : haystack) {
 939			if (conversation.getContact() == contact) {
 940				return conversation;
 941			}
 942		}
 943		return null;
 944	}
 945
 946	public Conversation find(final List<Conversation> haystack,
 947                             final Account account,
 948                             final Jid jid) {
 949		for (Conversation conversation : haystack) {
 950			if ((account == null || conversation.getAccount() == account)
 951					&& (conversation.getContactJid().toBareJid().equals(jid.toBareJid()))) {
 952				return conversation;
 953			}
 954		}
 955		return null;
 956	}
 957
 958	public Conversation findOrCreateConversation(final Account account, final Jid jid,
 959			final boolean muc) {
 960		Conversation conversation = find(account, jid);
 961		if (conversation != null) {
 962			return conversation;
 963		}
 964		conversation = databaseBackend.findConversation(account, jid);
 965		if (conversation != null) {
 966			conversation.setStatus(Conversation.STATUS_AVAILABLE);
 967			conversation.setAccount(account);
 968			if (muc) {
 969				conversation.setMode(Conversation.MODE_MULTI);
 970			} else {
 971				conversation.setMode(Conversation.MODE_SINGLE);
 972			}
 973			conversation.setMessages(databaseBackend.getMessages(conversation,
 974					50));
 975			this.databaseBackend.updateConversation(conversation);
 976		} else {
 977			String conversationName;
 978			Contact contact = account.getRoster().getContact(jid);
 979			if (contact != null) {
 980				conversationName = contact.getDisplayName();
 981			} else {
 982				conversationName = jid.getLocalpart();
 983			}
 984			if (muc) {
 985				conversation = new Conversation(conversationName, account, jid,
 986						Conversation.MODE_MULTI);
 987			} else {
 988				conversation = new Conversation(conversationName, account, jid,
 989						Conversation.MODE_SINGLE);
 990			}
 991			this.databaseBackend.createConversation(conversation);
 992		}
 993		this.conversations.add(conversation);
 994		updateConversationUi();
 995		return conversation;
 996	}
 997
 998	public void archiveConversation(Conversation conversation) {
 999		if (conversation.getMode() == Conversation.MODE_MULTI) {
1000			if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
1001				Bookmark bookmark = conversation.getBookmark();
1002				if (bookmark != null && bookmark.autojoin()) {
1003					bookmark.setAutojoin(false);
1004					pushBookmarks(bookmark.getAccount());
1005				}
1006			}
1007			leaveMuc(conversation);
1008		} else {
1009			conversation.endOtrIfNeeded();
1010		}
1011		this.databaseBackend.updateConversation(conversation);
1012		this.conversations.remove(conversation);
1013		updateConversationUi();
1014	}
1015
1016	public void clearConversationHistory(Conversation conversation) {
1017		this.databaseBackend.deleteMessagesInConversation(conversation);
1018		conversation.getMessages().clear();
1019		updateConversationUi();
1020	}
1021
1022	public int getConversationCount() {
1023		return this.databaseBackend.getConversationCount();
1024	}
1025
1026	public void createAccount(Account account) {
1027		databaseBackend.createAccount(account);
1028		this.accounts.add(account);
1029		this.reconnectAccount(account, false);
1030		updateAccountUi();
1031	}
1032
1033	public void updateAccount(Account account) {
1034		this.statusListener.onStatusChanged(account);
1035		databaseBackend.updateAccount(account);
1036		reconnectAccount(account, false);
1037		updateAccountUi();
1038		UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
1039	}
1040
1041	public void deleteAccount(Account account) {
1042		for (Conversation conversation : conversations) {
1043			if (conversation.getAccount() == account) {
1044				if (conversation.getMode() == Conversation.MODE_MULTI) {
1045					leaveMuc(conversation);
1046				} else if (conversation.getMode() == Conversation.MODE_SINGLE) {
1047					conversation.endOtrIfNeeded();
1048				}
1049				conversations.remove(conversation);
1050			}
1051		}
1052		if (account.getXmppConnection() != null) {
1053			this.disconnect(account, true);
1054		}
1055		databaseBackend.deleteAccount(account);
1056		this.accounts.remove(account);
1057		updateAccountUi();
1058		UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
1059	}
1060
1061	public void setOnConversationListChangedListener(
1062			OnConversationUpdate listener) {
1063		if (!isScreenOn()) {
1064			Log.d(Config.LOGTAG,
1065					"ignoring setOnConversationListChangedListener");
1066			return;
1067		}
1068		synchronized (this.convChangedListenerCount) {
1069			if (checkListeners()) {
1070				switchToForeground();
1071			}
1072			this.mOnConversationUpdate = listener;
1073			this.mNotificationService.setIsInForeground(true);
1074			this.convChangedListenerCount++;
1075		}
1076	}
1077
1078	public void removeOnConversationListChangedListener() {
1079		synchronized (this.convChangedListenerCount) {
1080			this.convChangedListenerCount--;
1081			if (this.convChangedListenerCount <= 0) {
1082				this.convChangedListenerCount = 0;
1083				this.mOnConversationUpdate = null;
1084				this.mNotificationService.setIsInForeground(false);
1085				if (checkListeners()) {
1086					switchToBackground();
1087				}
1088			}
1089		}
1090	}
1091
1092	public void setOnAccountListChangedListener(OnAccountUpdate listener) {
1093		if (!isScreenOn()) {
1094			Log.d(Config.LOGTAG, "ignoring setOnAccountListChangedListener");
1095			return;
1096		}
1097		synchronized (this.accountChangedListenerCount) {
1098			if (checkListeners()) {
1099				switchToForeground();
1100			}
1101			this.mOnAccountUpdate = listener;
1102			this.accountChangedListenerCount++;
1103		}
1104	}
1105
1106	public void removeOnAccountListChangedListener() {
1107		synchronized (this.accountChangedListenerCount) {
1108			this.accountChangedListenerCount--;
1109			if (this.accountChangedListenerCount <= 0) {
1110				this.mOnAccountUpdate = null;
1111				this.accountChangedListenerCount = 0;
1112				if (checkListeners()) {
1113					switchToBackground();
1114				}
1115			}
1116		}
1117	}
1118
1119	public void setOnRosterUpdateListener(OnRosterUpdate listener) {
1120		if (!isScreenOn()) {
1121			Log.d(Config.LOGTAG, "ignoring setOnRosterUpdateListener");
1122			return;
1123		}
1124		synchronized (this.rosterChangedListenerCount) {
1125			if (checkListeners()) {
1126				switchToForeground();
1127			}
1128			this.mOnRosterUpdate = listener;
1129			this.rosterChangedListenerCount++;
1130		}
1131	}
1132
1133	public void removeOnRosterUpdateListener() {
1134		synchronized (this.rosterChangedListenerCount) {
1135			this.rosterChangedListenerCount--;
1136			if (this.rosterChangedListenerCount <= 0) {
1137				this.rosterChangedListenerCount = 0;
1138				this.mOnRosterUpdate = null;
1139				if (checkListeners()) {
1140					switchToBackground();
1141				}
1142			}
1143		}
1144	}
1145
1146	private boolean checkListeners() {
1147		return (this.mOnAccountUpdate == null
1148				&& this.mOnConversationUpdate == null && this.mOnRosterUpdate == null);
1149	}
1150
1151	private void switchToForeground() {
1152		for (Account account : getAccounts()) {
1153			if (account.getStatus() == Account.STATUS_ONLINE) {
1154				XmppConnection connection = account.getXmppConnection();
1155				if (connection != null && connection.getFeatures().csi()) {
1156					connection.sendActive();
1157				}
1158			}
1159		}
1160		Log.d(Config.LOGTAG, "app switched into foreground");
1161	}
1162
1163	private void switchToBackground() {
1164		for (Account account : getAccounts()) {
1165			if (account.getStatus() == Account.STATUS_ONLINE) {
1166				XmppConnection connection = account.getXmppConnection();
1167				if (connection != null && connection.getFeatures().csi()) {
1168					connection.sendInactive();
1169				}
1170			}
1171		}
1172		this.mNotificationService.setIsInForeground(false);
1173		Log.d(Config.LOGTAG, "app switched into background");
1174	}
1175
1176	private boolean isScreenOn() {
1177		PowerManager pm = (PowerManager) this
1178				.getSystemService(Context.POWER_SERVICE);
1179		return pm.isScreenOn();
1180	}
1181
1182	public void connectMultiModeConversations(Account account) {
1183		List<Conversation> conversations = getConversations();
1184        for (Conversation conversation : conversations) {
1185            if ((conversation.getMode() == Conversation.MODE_MULTI)
1186                    && (conversation.getAccount() == account)) {
1187                joinMuc(conversation);
1188            }
1189        }
1190	}
1191
1192	public void joinMuc(Conversation conversation) {
1193		Account account = conversation.getAccount();
1194		account.pendingConferenceJoins.remove(conversation);
1195		account.pendingConferenceLeaves.remove(conversation);
1196		if (account.getStatus() == Account.STATUS_ONLINE) {
1197			Log.d(Config.LOGTAG,
1198					"joining conversation " + conversation.getContactJid());
1199			String nick = conversation.getMucOptions().getProposedNick();
1200			conversation.getMucOptions().setJoinNick(nick);
1201			PresencePacket packet = new PresencePacket();
1202			final Jid joinJid = conversation.getMucOptions().getJoinJid();
1203            packet.setTo(conversation.getMucOptions().getJoinJid());
1204			Element x = new Element("x");
1205			x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
1206			if (conversation.getMucOptions().getPassword() != null) {
1207				Element password = x.addChild("password");
1208				password.setContent(conversation.getMucOptions().getPassword());
1209			}
1210			String sig = account.getPgpSignature();
1211			if (sig != null) {
1212				packet.addChild("status").setContent("online");
1213				packet.addChild("x", "jabber:x:signed").setContent(sig);
1214			}
1215			if (conversation.getMessages().size() != 0) {
1216				final SimpleDateFormat mDateFormat = new SimpleDateFormat(
1217						"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
1218				mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
1219				Date date = new Date(conversation.getLatestMessage()
1220						.getTimeSent() + 1000);
1221				x.addChild("history").setAttribute("since",
1222						mDateFormat.format(date));
1223			}
1224			packet.addChild(x);
1225			sendPresencePacket(account, packet);
1226			if (!joinJid.equals(conversation.getContactJid())) {
1227				conversation.setContactJid(joinJid);
1228				databaseBackend.updateConversation(conversation);
1229			}
1230		} else {
1231			account.pendingConferenceJoins.add(conversation);
1232		}
1233	}
1234
1235	private OnRenameListener renameListener = null;
1236	private IqGenerator mIqGenerator = new IqGenerator(this);
1237
1238	public void setOnRenameListener(OnRenameListener listener) {
1239		this.renameListener = listener;
1240	}
1241
1242	public void providePasswordForMuc(Conversation conversation, String password) {
1243		if (conversation.getMode() == Conversation.MODE_MULTI) {
1244			conversation.getMucOptions().setPassword(password);
1245			if (conversation.getBookmark() != null) {
1246				conversation.getBookmark().setAutojoin(true);
1247				pushBookmarks(conversation.getAccount());
1248			}
1249			databaseBackend.updateConversation(conversation);
1250			joinMuc(conversation);
1251		}
1252	}
1253
1254	public void renameInMuc(final Conversation conversation, final String nick) {
1255		final MucOptions options = conversation.getMucOptions();
1256		options.setJoinNick(nick);
1257		if (options.online()) {
1258			Account account = conversation.getAccount();
1259			options.setOnRenameListener(new OnRenameListener() {
1260
1261				@Override
1262				public void onRename(boolean success) {
1263					if (renameListener != null) {
1264						renameListener.onRename(success);
1265					}
1266					if (success) {
1267						conversation.setContactJid(conversation.getMucOptions()
1268								.getJoinJid());
1269						databaseBackend.updateConversation(conversation);
1270						Bookmark bookmark = conversation.getBookmark();
1271						if (bookmark != null) {
1272							bookmark.setNick(nick);
1273							pushBookmarks(bookmark.getAccount());
1274						}
1275					}
1276				}
1277			});
1278			options.flagAboutToRename();
1279			PresencePacket packet = new PresencePacket();
1280			packet.setTo(options.getJoinJid());
1281			packet.setFrom(conversation.getAccount().getJid());
1282
1283			String sig = account.getPgpSignature();
1284			if (sig != null) {
1285				packet.addChild("status").setContent("online");
1286				packet.addChild("x", "jabber:x:signed").setContent(sig);
1287			}
1288			sendPresencePacket(account, packet);
1289		} else {
1290			conversation.setContactJid(options.getJoinJid());
1291			databaseBackend.updateConversation(conversation);
1292			if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
1293				Bookmark bookmark = conversation.getBookmark();
1294				if (bookmark != null) {
1295					bookmark.setNick(nick);
1296					pushBookmarks(bookmark.getAccount());
1297				}
1298				joinMuc(conversation);
1299			}
1300		}
1301	}
1302
1303	public void leaveMuc(Conversation conversation) {
1304		Account account = conversation.getAccount();
1305		account.pendingConferenceJoins.remove(conversation);
1306		account.pendingConferenceLeaves.remove(conversation);
1307		if (account.getStatus() == Account.STATUS_ONLINE) {
1308			PresencePacket packet = new PresencePacket();
1309			packet.setTo(conversation.getMucOptions().getJoinJid());
1310			packet.setFrom(conversation.getAccount().getJid());
1311			packet.setAttribute("type", "unavailable");
1312			sendPresencePacket(conversation.getAccount(), packet);
1313			conversation.getMucOptions().setOffline();
1314			conversation.deregisterWithBookmark();
1315			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
1316					+ ": leaving muc " + conversation.getContactJid());
1317		} else {
1318			account.pendingConferenceLeaves.add(conversation);
1319		}
1320	}
1321
1322	public void disconnect(Account account, boolean force) {
1323		if ((account.getStatus() == Account.STATUS_ONLINE)
1324				|| (account.getStatus() == Account.STATUS_DISABLED)) {
1325			if (!force) {
1326				List<Conversation> conversations = getConversations();
1327                for (Conversation conversation : conversations) {
1328                    if (conversation.getAccount() == account) {
1329                        if (conversation.getMode() == Conversation.MODE_MULTI) {
1330                            leaveMuc(conversation);
1331                        } else {
1332                            if (conversation.endOtrIfNeeded()) {
1333                                Log.d(Config.LOGTAG, account.getJid().toBareJid()
1334                                        + ": ended otr session with "
1335                                        + conversation.getContactJid());
1336                            }
1337                        }
1338                    }
1339                }
1340			}
1341			account.getXmppConnection().disconnect(force);
1342		}
1343	}
1344
1345	@Override
1346	public IBinder onBind(Intent intent) {
1347		return mBinder;
1348	}
1349
1350	public void updateMessage(Message message) {
1351		databaseBackend.updateMessage(message);
1352		updateConversationUi();
1353	}
1354
1355	protected void syncDirtyContacts(Account account) {
1356		for (Contact contact : account.getRoster().getContacts()) {
1357			if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
1358				pushContactToServer(contact);
1359			}
1360			if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
1361				deleteContactOnServer(contact);
1362			}
1363		}
1364	}
1365
1366	public void createContact(Contact contact) {
1367		SharedPreferences sharedPref = getPreferences();
1368		boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1369		if (autoGrant) {
1370			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
1371			contact.setOption(Contact.Options.ASKING);
1372		}
1373		pushContactToServer(contact);
1374	}
1375
1376	public void onOtrSessionEstablished(Conversation conversation) {
1377		Account account = conversation.getAccount();
1378		List<Message> messages = conversation.getMessages();
1379		Session otrSession = conversation.getOtrSession();
1380		Log.d(Config.LOGTAG,
1381				account.getJid().toBareJid() + " otr session established with "
1382						+ conversation.getContactJid() + "/"
1383						+ otrSession.getSessionID().getUserID());
1384        for (Message msg : messages) {
1385            if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING)
1386                    && (msg.getEncryption() == Message.ENCRYPTION_OTR)) {
1387				SessionID id = otrSession.getSessionID();
1388				try {
1389					msg.setCounterpart(Jid.fromString(id.getAccountID()+"/"+id.getUserID()));
1390				} catch (InvalidJidException e) {
1391					break;
1392				}
1393                if (msg.getType() == Message.TYPE_TEXT) {
1394                    MessagePacket outPacket = mMessageGenerator
1395                            .generateOtrChat(msg, true);
1396                    if (outPacket != null) {
1397                        msg.setStatus(Message.STATUS_SEND);
1398                        databaseBackend.updateMessage(msg);
1399                        sendMessagePacket(account, outPacket);
1400                    }
1401                } else if (msg.getType() == Message.TYPE_IMAGE) {
1402                    mJingleConnectionManager.createNewConnection(msg);
1403                }
1404            }
1405        }
1406		updateConversationUi();
1407	}
1408
1409	public boolean renewSymmetricKey(Conversation conversation) {
1410		Account account = conversation.getAccount();
1411		byte[] symmetricKey = new byte[32];
1412		this.mRandom.nextBytes(symmetricKey);
1413		Session otrSession = conversation.getOtrSession();
1414		if (otrSession != null) {
1415			MessagePacket packet = new MessagePacket();
1416			packet.setType(MessagePacket.TYPE_CHAT);
1417			packet.setFrom(account.getJid());
1418			packet.addChild("private", "urn:xmpp:carbons:2");
1419			packet.addChild("no-copy", "urn:xmpp:hints");
1420			packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
1421					+ otrSession.getSessionID().getUserID());
1422			try {
1423				packet.setBody(otrSession
1424						.transformSending(CryptoHelper.FILETRANSFER
1425								+ CryptoHelper.bytesToHex(symmetricKey)));
1426				sendMessagePacket(account, packet);
1427				conversation.setSymmetricKey(symmetricKey);
1428				return true;
1429			} catch (OtrException e) {
1430				return false;
1431			}
1432		}
1433		return false;
1434	}
1435
1436	public void pushContactToServer(Contact contact) {
1437		contact.resetOption(Contact.Options.DIRTY_DELETE);
1438		contact.setOption(Contact.Options.DIRTY_PUSH);
1439		Account account = contact.getAccount();
1440		if (account.getStatus() == Account.STATUS_ONLINE) {
1441			boolean ask = contact.getOption(Contact.Options.ASKING);
1442			boolean sendUpdates = contact
1443					.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
1444					&& contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
1445			IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1446			iq.query("jabber:iq:roster").addChild(contact.asElement());
1447			account.getXmppConnection().sendIqPacket(iq, null);
1448			if (sendUpdates) {
1449				sendPresencePacket(account,
1450						mPresenceGenerator.sendPresenceUpdatesTo(contact));
1451			}
1452			if (ask) {
1453				sendPresencePacket(account,
1454						mPresenceGenerator.requestPresenceUpdatesFrom(contact));
1455			}
1456		}
1457	}
1458
1459	public void publishAvatar(Account account, Uri image,
1460			final UiCallback<Avatar> callback) {
1461		final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
1462		final int size = Config.AVATAR_SIZE;
1463		final Avatar avatar = getFileBackend()
1464				.getPepAvatar(image, size, format);
1465		if (avatar != null) {
1466			avatar.height = size;
1467			avatar.width = size;
1468			if (format.equals(Bitmap.CompressFormat.WEBP)) {
1469				avatar.type = "image/webp";
1470			} else if (format.equals(Bitmap.CompressFormat.JPEG)) {
1471				avatar.type = "image/jpeg";
1472			} else if (format.equals(Bitmap.CompressFormat.PNG)) {
1473				avatar.type = "image/png";
1474			}
1475			if (!getFileBackend().save(avatar)) {
1476				callback.error(R.string.error_saving_avatar, avatar);
1477				return;
1478			}
1479			IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
1480			this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1481
1482				@Override
1483				public void onIqPacketReceived(Account account, IqPacket result) {
1484					if (result.getType() == IqPacket.TYPE_RESULT) {
1485						IqPacket packet = XmppConnectionService.this.mIqGenerator
1486								.publishAvatarMetadata(avatar);
1487						sendIqPacket(account, packet, new OnIqPacketReceived() {
1488
1489							@Override
1490							public void onIqPacketReceived(Account account,
1491									IqPacket result) {
1492								if (result.getType() == IqPacket.TYPE_RESULT) {
1493									if (account.setAvatar(avatar.getFilename())) {
1494										databaseBackend.updateAccount(account);
1495									}
1496									callback.success(avatar);
1497								} else {
1498									callback.error(
1499											R.string.error_publish_avatar_server_reject,
1500											avatar);
1501								}
1502							}
1503						});
1504					} else {
1505						callback.error(
1506								R.string.error_publish_avatar_server_reject,
1507								avatar);
1508					}
1509				}
1510			});
1511		} else {
1512			callback.error(R.string.error_publish_avatar_converting, null);
1513		}
1514	}
1515
1516	public void fetchAvatar(Account account, Avatar avatar) {
1517		fetchAvatar(account, avatar, null);
1518	}
1519
1520	public void fetchAvatar(Account account, final Avatar avatar,
1521			final UiCallback<Avatar> callback) {
1522		IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar);
1523		sendIqPacket(account, packet, new OnIqPacketReceived() {
1524
1525			@Override
1526			public void onIqPacketReceived(Account account, IqPacket result) {
1527				final String ERROR = account.getJid().toBareJid()
1528						+ ": fetching avatar for " + avatar.owner + " failed ";
1529				if (result.getType() == IqPacket.TYPE_RESULT) {
1530					avatar.image = mIqParser.avatarData(result);
1531					if (avatar.image != null) {
1532						if (getFileBackend().save(avatar)) {
1533							if (account.getJid().toBareJid().equals(avatar.owner)) {
1534								if (account.setAvatar(avatar.getFilename())) {
1535									databaseBackend.updateAccount(account);
1536								}
1537								getAvatarService().clear(account);
1538								updateConversationUi();
1539								updateAccountUi();
1540							} else {
1541								Contact contact = account.getRoster()
1542										.getContact(avatar.owner);
1543								contact.setAvatar(avatar.getFilename());
1544								getAvatarService().clear(contact);
1545								updateConversationUi();
1546								updateRosterUi();
1547							}
1548							if (callback != null) {
1549								callback.success(avatar);
1550							}
1551							Log.d(Config.LOGTAG, account.getJid().toBareJid()
1552									+ ": succesfully fetched avatar for "
1553									+ avatar.owner);
1554							return;
1555						}
1556					} else {
1557
1558						Log.d(Config.LOGTAG, ERROR + "(parsing error)");
1559					}
1560				} else {
1561					Element error = result.findChild("error");
1562					if (error == null) {
1563						Log.d(Config.LOGTAG, ERROR + "(server error)");
1564					} else {
1565						Log.d(Config.LOGTAG, ERROR + error.toString());
1566					}
1567				}
1568				if (callback != null) {
1569					callback.error(0, null);
1570				}
1571
1572			}
1573		});
1574	}
1575
1576	public void checkForAvatar(Account account,
1577			final UiCallback<Avatar> callback) {
1578		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
1579		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1580
1581			@Override
1582			public void onIqPacketReceived(Account account, IqPacket packet) {
1583				if (packet.getType() == IqPacket.TYPE_RESULT) {
1584					Element pubsub = packet.findChild("pubsub",
1585							"http://jabber.org/protocol/pubsub");
1586					if (pubsub != null) {
1587						Element items = pubsub.findChild("items");
1588						if (items != null) {
1589							Avatar avatar = Avatar.parseMetadata(items);
1590							if (avatar != null) {
1591								avatar.owner = account.getJid().toBareJid();
1592								if (fileBackend.isAvatarCached(avatar)) {
1593									if (account.setAvatar(avatar.getFilename())) {
1594										databaseBackend.updateAccount(account);
1595									}
1596									getAvatarService().clear(account);
1597									callback.success(avatar);
1598								} else {
1599									fetchAvatar(account, avatar, callback);
1600								}
1601								return;
1602							}
1603						}
1604					}
1605				}
1606				callback.error(0, null);
1607			}
1608		});
1609	}
1610
1611	public void deleteContactOnServer(Contact contact) {
1612		contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
1613		contact.resetOption(Contact.Options.DIRTY_PUSH);
1614		contact.setOption(Contact.Options.DIRTY_DELETE);
1615		Account account = contact.getAccount();
1616		if (account.getStatus() == Account.STATUS_ONLINE) {
1617			IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1618			Element item = iq.query("jabber:iq:roster").addChild("item");
1619			item.setAttribute("jid", contact.getJid().toString());
1620			item.setAttribute("subscription", "remove");
1621			account.getXmppConnection().sendIqPacket(iq, null);
1622		}
1623	}
1624
1625	public void updateConversation(Conversation conversation) {
1626		this.databaseBackend.updateConversation(conversation);
1627	}
1628
1629	public void reconnectAccount(final Account account, final boolean force) {
1630		new Thread(new Runnable() {
1631
1632			@Override
1633			public void run() {
1634				if (account.getXmppConnection() != null) {
1635					disconnect(account, force);
1636				}
1637				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1638					if (account.getXmppConnection() == null) {
1639						account.setXmppConnection(createConnection(account));
1640					}
1641					Thread thread = new Thread(account.getXmppConnection());
1642					thread.start();
1643					scheduleWakeupCall((int) (Config.CONNECT_TIMEOUT * 1.2),
1644							false);
1645				} else {
1646					account.getRoster().clearPresences();
1647					account.setXmppConnection(null);
1648				}
1649			}
1650		}).start();
1651	}
1652
1653	public void invite(Conversation conversation, String contact) {
1654		MessagePacket packet = mMessageGenerator.invite(conversation, contact);
1655		sendMessagePacket(conversation.getAccount(), packet);
1656	}
1657
1658	public void resetSendingToWaiting(Account account) {
1659		for (Conversation conversation : getConversations()) {
1660			if (conversation.getAccount() == account) {
1661				for (Message message : conversation.getMessages()) {
1662					if (message.getType() != Message.TYPE_IMAGE
1663							&& message.getStatus() == Message.STATUS_UNSEND) {
1664						markMessage(message, Message.STATUS_WAITING);
1665					}
1666				}
1667			}
1668		}
1669	}
1670
1671	public boolean markMessage(final Account account, final Jid recipient, final String uuid,
1672			final int status) {
1673		if (uuid == null) {
1674			return false;
1675		} else {
1676			for (Conversation conversation : getConversations()) {
1677				if (conversation.getContactJid().equals(recipient)
1678						&& conversation.getAccount().equals(account)) {
1679					return markMessage(conversation, uuid, status);
1680				}
1681			}
1682			return false;
1683		}
1684	}
1685
1686	public boolean markMessage(Conversation conversation, String uuid,
1687			int status) {
1688		if (uuid == null) {
1689			return false;
1690		} else {
1691			for (Message message : conversation.getMessages()) {
1692				if (uuid.equals(message.getUuid())
1693						|| (message.getStatus() >= Message.STATUS_SEND && uuid
1694								.equals(message.getRemoteMsgId()))) {
1695					markMessage(message, status);
1696					return true;
1697				}
1698			}
1699			return false;
1700		}
1701	}
1702
1703	public void markMessage(Message message, int status) {
1704		if (status == Message.STATUS_SEND_FAILED
1705				&& (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
1706						.getStatus() == Message.STATUS_SEND_DISPLAYED)) {
1707			return;
1708		}
1709		message.setStatus(status);
1710		databaseBackend.updateMessage(message);
1711		updateConversationUi();
1712	}
1713
1714	public SharedPreferences getPreferences() {
1715		return PreferenceManager
1716				.getDefaultSharedPreferences(getApplicationContext());
1717	}
1718
1719	public boolean forceEncryption() {
1720		return getPreferences().getBoolean("force_encryption", false);
1721	}
1722
1723	public boolean confirmMessages() {
1724		return getPreferences().getBoolean("confirm_messages", true);
1725	}
1726
1727	public boolean saveEncryptedMessages() {
1728		return !getPreferences().getBoolean("dont_save_encrypted", false);
1729	}
1730
1731	public boolean indicateReceived() {
1732		return getPreferences().getBoolean("indicate_received", false);
1733	}
1734
1735	public void updateConversationUi() {
1736		if (mOnConversationUpdate != null) {
1737			mOnConversationUpdate.onConversationUpdate();
1738		}
1739	}
1740
1741	public void updateAccountUi() {
1742		if (mOnAccountUpdate != null) {
1743			mOnAccountUpdate.onAccountUpdate();
1744		}
1745	}
1746
1747	public void updateRosterUi() {
1748		if (mOnRosterUpdate != null) {
1749			mOnRosterUpdate.onRosterUpdate();
1750		}
1751	}
1752
1753	public Account findAccountByJid(final Jid accountJid) {
1754		for (Account account : this.accounts) {
1755			if (account.getJid().toBareJid().equals(accountJid)) {
1756				return account;
1757			}
1758		}
1759		return null;
1760	}
1761
1762	public Conversation findConversationByUuid(String uuid) {
1763		for (Conversation conversation : getConversations()) {
1764			if (conversation.getUuid().equals(uuid)) {
1765				return conversation;
1766			}
1767		}
1768		return null;
1769	}
1770
1771	public void markRead(Conversation conversation, boolean calledByUi) {
1772		mNotificationService.clear(conversation);
1773		String id = conversation.getLatestMarkableMessageId();
1774		conversation.markRead();
1775		if (confirmMessages() && id != null && calledByUi) {
1776			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
1777					+ ": sending read marker for " + conversation.getName());
1778			Account account = conversation.getAccount();
1779			final Jid to = conversation.getContactJid();
1780			this.sendMessagePacket(conversation.getAccount(),
1781					mMessageGenerator.confirm(account, to, id));
1782		}
1783		if (!calledByUi) {
1784			updateConversationUi();
1785		}
1786	}
1787
1788	public void failWaitingOtrMessages(Conversation conversation) {
1789		for (Message message : conversation.getMessages()) {
1790			if (message.getEncryption() == Message.ENCRYPTION_OTR
1791					&& message.getStatus() == Message.STATUS_WAITING) {
1792				markMessage(message, Message.STATUS_SEND_FAILED);
1793			}
1794		}
1795	}
1796
1797	public SecureRandom getRNG() {
1798		return this.mRandom;
1799	}
1800
1801	public MemorizingTrustManager getMemorizingTrustManager() {
1802		return this.mMemorizingTrustManager;
1803	}
1804
1805	public PowerManager getPowerManager() {
1806		return this.pm;
1807	}
1808
1809	public LruCache<String, Bitmap> getBitmapCache() {
1810		return this.mBitmapCache;
1811	}
1812
1813	public void replyWithNotAcceptable(Account account, MessagePacket packet) {
1814		if (account.getStatus() == Account.STATUS_ONLINE) {
1815			MessagePacket error = this.mMessageGenerator
1816					.generateNotAcceptable(packet);
1817			sendMessagePacket(account, error);
1818		}
1819	}
1820
1821	public void syncRosterToDisk(final Account account) {
1822		new Thread(new Runnable() {
1823
1824			@Override
1825			public void run() {
1826				databaseBackend.writeRoster(account.getRoster());
1827			}
1828		}).start();
1829
1830	}
1831
1832	public List<String> getKnownHosts() {
1833		List<String> hosts = new ArrayList<>();
1834		for (Account account : getAccounts()) {
1835			if (!hosts.contains(account.getServer().toString())) {
1836				hosts.add(account.getServer().toString());
1837			}
1838			for (Contact contact : account.getRoster().getContacts()) {
1839				if (contact.showInRoster()) {
1840					final String server = contact.getServer().toString();
1841					if (server != null && !hosts.contains(server)) {
1842						hosts.add(server);
1843					}
1844				}
1845			}
1846		}
1847		return hosts;
1848	}
1849
1850	public List<String> getKnownConferenceHosts() {
1851		ArrayList<String> mucServers = new ArrayList<>();
1852		for (Account account : accounts) {
1853			if (account.getXmppConnection() != null) {
1854				String server = account.getXmppConnection().getMucServer();
1855				if (server != null && !mucServers.contains(server)) {
1856					mucServers.add(server);
1857				}
1858			}
1859		}
1860		return mucServers;
1861	}
1862
1863	public void sendMessagePacket(Account account, MessagePacket packet) {
1864		XmppConnection connection = account.getXmppConnection();
1865		if (connection != null) {
1866			connection.sendMessagePacket(packet);
1867		}
1868	}
1869
1870	public void sendPresencePacket(Account account, PresencePacket packet) {
1871		XmppConnection connection = account.getXmppConnection();
1872		if (connection != null) {
1873			connection.sendPresencePacket(packet);
1874		}
1875	}
1876
1877	public void sendIqPacket(Account account, IqPacket packet,
1878			OnIqPacketReceived callback) {
1879		XmppConnection connection = account.getXmppConnection();
1880		if (connection != null) {
1881			connection.sendIqPacket(packet, callback);
1882		}
1883	}
1884
1885	public MessageGenerator getMessageGenerator() {
1886		return this.mMessageGenerator;
1887	}
1888
1889	public PresenceGenerator getPresenceGenerator() {
1890		return this.mPresenceGenerator;
1891	}
1892
1893	public IqGenerator getIqGenerator() {
1894		return this.mIqGenerator;
1895	}
1896
1897	public JingleConnectionManager getJingleConnectionManager() {
1898		return this.mJingleConnectionManager;
1899	}
1900
1901	public interface OnConversationUpdate {
1902		public void onConversationUpdate();
1903	}
1904
1905	public interface OnAccountUpdate {
1906		public void onAccountUpdate();
1907	}
1908
1909	public interface OnRosterUpdate {
1910		public void onRosterUpdate();
1911	}
1912
1913	public List<Contact> findContacts(String jid) {
1914		ArrayList<Contact> contacts = new ArrayList<>();
1915		for (Account account : getAccounts()) {
1916			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1917				Contact contact = account.getRoster().getContactFromRoster(jid);
1918				if (contact != null) {
1919					contacts.add(contact);
1920				}
1921			}
1922		}
1923		return contacts;
1924	}
1925
1926	public NotificationService getNotificationService() {
1927		return this.mNotificationService;
1928	}
1929
1930	public HttpConnectionManager getHttpConnectionManager() {
1931		return this.mHttpConnectionManager;
1932	}
1933
1934	private class DeletedDownloadable implements Downloadable {
1935
1936		@Override
1937		public boolean start() {
1938			return false;
1939		}
1940
1941		@Override
1942		public int getStatus() {
1943			return Downloadable.STATUS_DELETED;
1944		}
1945
1946		@Override
1947		public long getFileSize() {
1948			return 0;
1949		}
1950
1951	}
1952
1953	public void resendFailedMessages(Message message) {
1954		List<Message> messages = new ArrayList<>();
1955		Message current = message;
1956		while(current.getStatus() == Message.STATUS_SEND_FAILED) {
1957			messages.add(current);
1958			if (current.mergeable(current.next())) {
1959				current = current.next();
1960			} else {
1961				break;
1962			}
1963		}
1964		for(Message msg: messages) {
1965			markMessage(msg, Message.STATUS_WAITING);
1966			this.resendMessage(msg);
1967		}
1968	}
1969}