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