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			if (message.getType() == Message.TYPE_IMAGE) {
 522				if (message.getPresence() != null) {
 523					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 524						if (!conv.hasValidOtrSession()
 525								&& (message.getPresence() != null)) {
 526							conv.startOtrSession(this, message.getPresence(),
 527									true);
 528							message.setStatus(Message.STATUS_WAITING);
 529						} else if (conv.hasValidOtrSession()
 530								&& conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 531							mJingleConnectionManager
 532									.createNewConnection(message);
 533						} else if (message.getPresence() == null) {
 534							message.setStatus(Message.STATUS_WAITING);
 535						}
 536					} else {
 537						mJingleConnectionManager.createNewConnection(message);
 538					}
 539				} else {
 540					message.setStatus(Message.STATUS_WAITING);
 541				}
 542			} else {
 543				if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 544					if (!conv.hasValidOtrSession()
 545							&& (message.getPresence() != null)) {
 546						conv.startOtrSession(this, message.getPresence(), true);
 547						message.setStatus(Message.STATUS_WAITING);
 548					} else if (conv.hasValidOtrSession()
 549							&& conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 550						message.setPresence(conv.getOtrSession().getSessionID()
 551								.getUserID());
 552						packet = mMessageGenerator.generateOtrChat(message);
 553						send = true;
 554
 555					} else if (message.getPresence() == null) {
 556						message.setStatus(Message.STATUS_WAITING);
 557					}
 558				} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 559					message.getConversation().endOtrIfNeeded();
 560					failWaitingOtrMessages(message.getConversation());
 561					packet = mMessageGenerator.generatePgpChat(message);
 562					send = true;
 563				} else {
 564					message.getConversation().endOtrIfNeeded();
 565					failWaitingOtrMessages(message.getConversation());
 566					packet = mMessageGenerator.generateChat(message);
 567					send = true;
 568				}
 569			}
 570		} else {
 571			message.setStatus(Message.STATUS_WAITING);
 572			if (message.getType() == Message.TYPE_TEXT) {
 573				if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 574					String pgpBody = message.getEncryptedBody();
 575					String decryptedBody = message.getBody();
 576					message.setBody(pgpBody);
 577					message.setEncryption(Message.ENCRYPTION_PGP);
 578					databaseBackend.createMessage(message);
 579					saveInDb = false;
 580					message.setBody(decryptedBody);
 581					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 582				} else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 583					if (conv.hasValidOtrSession()) {
 584						message.setPresence(conv.getOtrSession().getSessionID()
 585								.getUserID());
 586					} else if (!conv.hasValidOtrSession()
 587							&& message.getPresence() != null) {
 588						conv.startOtrSession(this, message.getPresence(), false);
 589					}
 590				}
 591			}
 592
 593		}
 594		conv.getMessages().add(message);
 595		if (!account.getXmppConnection().getFeatures().sm()
 596				&& conv.getMode() != Conversation.MODE_MULTI) {
 597			message.setStatus(Message.STATUS_SEND);
 598		}
 599		if (saveInDb) {
 600			if (message.getEncryption() == Message.ENCRYPTION_NONE
 601					|| saveEncryptedMessages()) {
 602				databaseBackend.createMessage(message);
 603			}
 604		}
 605		if ((send) && (packet != null)) {
 606			sendMessagePacket(account, packet);
 607		}
 608		updateConversationUi();
 609	}
 610
 611	private void sendUnsendMessages(Conversation conversation) {
 612		for (int i = 0; i < conversation.getMessages().size(); ++i) {
 613			int status = conversation.getMessages().get(i).getStatus();
 614			if (status == Message.STATUS_WAITING) {
 615				resendMessage(conversation.getMessages().get(i));
 616			}
 617		}
 618	}
 619
 620	private void resendMessage(Message message) {
 621		Account account = message.getConversation().getAccount();
 622		MessagePacket packet = null;
 623		if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 624			Presences presences = message.getConversation().getContact()
 625					.getPresences();
 626			if (!message.getConversation().hasValidOtrSession()) {
 627				if ((message.getPresence() != null)
 628						&& (presences.has(message.getPresence()))) {
 629					message.getConversation().startOtrSession(this,
 630							message.getPresence(), true);
 631				} else {
 632					if (presences.size() == 1) {
 633						String presence = presences.asStringArray()[0];
 634						message.getConversation().startOtrSession(this,
 635								presence, true);
 636					}
 637				}
 638			} else {
 639				if (message.getConversation().getOtrSession()
 640						.getSessionStatus() == SessionStatus.ENCRYPTED) {
 641					if (message.getType() == Message.TYPE_TEXT) {
 642						packet = mMessageGenerator.generateOtrChat(message,
 643								true);
 644					} else if (message.getType() == Message.TYPE_IMAGE) {
 645						mJingleConnectionManager.createNewConnection(message);
 646					}
 647				}
 648			}
 649		} else if (message.getType() == Message.TYPE_TEXT) {
 650			if (message.getEncryption() == Message.ENCRYPTION_NONE) {
 651				packet = mMessageGenerator.generateChat(message, true);
 652			} else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED)
 653					|| (message.getEncryption() == Message.ENCRYPTION_PGP)) {
 654				packet = mMessageGenerator.generatePgpChat(message, true);
 655			}
 656		} else if (message.getType() == Message.TYPE_IMAGE) {
 657			Presences presences = message.getConversation().getContact()
 658					.getPresences();
 659			if ((message.getPresence() != null)
 660					&& (presences.has(message.getPresence()))) {
 661				markMessage(message, Message.STATUS_OFFERED);
 662				mJingleConnectionManager.createNewConnection(message);
 663			} else {
 664				if (presences.size() == 1) {
 665					String presence = presences.asStringArray()[0];
 666					message.setPresence(presence);
 667					markMessage(message, Message.STATUS_OFFERED);
 668					mJingleConnectionManager.createNewConnection(message);
 669				}
 670			}
 671		}
 672		if (packet != null) {
 673			if (!account.getXmppConnection().getFeatures().sm()
 674					&& message.getConversation().getMode() != Conversation.MODE_MULTI) {
 675				markMessage(message, Message.STATUS_SEND);
 676			} else {
 677				markMessage(message, Message.STATUS_UNSEND);
 678			}
 679			sendMessagePacket(account, packet);
 680		}
 681	}
 682
 683	public void fetchRosterFromServer(Account account) {
 684		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
 685		if (!"".equals(account.getRosterVersion())) {
 686			Log.d(Config.LOGTAG, account.getJid()
 687					+ ": fetching roster version " + account.getRosterVersion());
 688		} else {
 689			Log.d(Config.LOGTAG, account.getJid() + ": fetching roster");
 690		}
 691		iqPacket.query("jabber:iq:roster").setAttribute("ver",
 692				account.getRosterVersion());
 693		account.getXmppConnection().sendIqPacket(iqPacket,
 694				new OnIqPacketReceived() {
 695
 696					@Override
 697					public void onIqPacketReceived(final Account account,
 698							IqPacket packet) {
 699						Element query = packet.findChild("query");
 700						if (query != null) {
 701							account.getRoster().markAllAsNotInRoster();
 702							mIqParser.rosterItems(account, query);
 703						}
 704					}
 705				});
 706	}
 707
 708	public void fetchBookmarks(Account account) {
 709		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
 710		Element query = iqPacket.query("jabber:iq:private");
 711		query.addChild("storage", "storage:bookmarks");
 712		OnIqPacketReceived callback = new OnIqPacketReceived() {
 713
 714			@Override
 715			public void onIqPacketReceived(Account account, IqPacket packet) {
 716				Element query = packet.query();
 717				List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>();
 718				Element storage = query.findChild("storage",
 719						"storage:bookmarks");
 720				if (storage != null) {
 721					for (Element item : storage.getChildren()) {
 722						if (item.getName().equals("conference")) {
 723							Bookmark bookmark = Bookmark.parse(item, account);
 724							bookmarks.add(bookmark);
 725							Conversation conversation = find(bookmark);
 726							if (conversation != null) {
 727								conversation.setBookmark(bookmark);
 728							} else {
 729								if (bookmark.autojoin()) {
 730									conversation = findOrCreateConversation(
 731											account, bookmark.getJid(), true);
 732									conversation.setBookmark(bookmark);
 733									joinMuc(conversation);
 734								}
 735							}
 736						}
 737					}
 738				}
 739				account.setBookmarks(bookmarks);
 740			}
 741		};
 742		sendIqPacket(account, iqPacket, callback);
 743
 744	}
 745
 746	public void pushBookmarks(Account account) {
 747		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_SET);
 748		Element query = iqPacket.query("jabber:iq:private");
 749		Element storage = query.addChild("storage", "storage:bookmarks");
 750		for (Bookmark bookmark : account.getBookmarks()) {
 751			storage.addChild(bookmark);
 752		}
 753		sendIqPacket(account, iqPacket, null);
 754	}
 755
 756	private void mergePhoneContactsWithRoster() {
 757		PhoneHelper.loadPhoneContacts(getApplicationContext(),
 758				new OnPhoneContactsLoadedListener() {
 759					@Override
 760					public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
 761						for (Account account : accounts) {
 762							account.getRoster().clearSystemAccounts();
 763						}
 764						for (Bundle phoneContact : phoneContacts) {
 765							for (Account account : accounts) {
 766								String jid = phoneContact.getString("jid");
 767								Contact contact = account.getRoster()
 768										.getContact(jid);
 769								String systemAccount = phoneContact
 770										.getInt("phoneid")
 771										+ "#"
 772										+ phoneContact.getString("lookup");
 773								contact.setSystemAccount(systemAccount);
 774								contact.setPhotoUri(phoneContact
 775										.getString("photouri"));
 776								contact.setSystemName(phoneContact
 777										.getString("displayname"));
 778							}
 779						}
 780					}
 781				});
 782	}
 783
 784	public List<Conversation> getConversations() {
 785		if (this.conversations == null) {
 786			Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
 787			for (Account account : this.accounts) {
 788				accountLookupTable.put(account.getUuid(), account);
 789			}
 790			this.conversations = databaseBackend
 791					.getConversations(Conversation.STATUS_AVAILABLE);
 792			for (Conversation conv : this.conversations) {
 793				Account account = accountLookupTable.get(conv.getAccountUuid());
 794				conv.setAccount(account);
 795				conv.setMessages(databaseBackend.getMessages(conv, 50));
 796			}
 797		}
 798
 799		return this.conversations;
 800	}
 801
 802	public void populateWithOrderedConversations(List<Conversation> list) {
 803		populateWithOrderedConversations(list, true);
 804	}
 805
 806	public void populateWithOrderedConversations(List<Conversation> list,
 807			boolean includeConferences) {
 808		list.clear();
 809		if (includeConferences) {
 810			list.addAll(getConversations());
 811		} else {
 812			for (Conversation conversation : getConversations()) {
 813				if (conversation.getMode() == Conversation.MODE_SINGLE) {
 814					list.add(conversation);
 815				}
 816			}
 817		}
 818		Collections.sort(list, new Comparator<Conversation>() {
 819			@Override
 820			public int compare(Conversation lhs, Conversation rhs) {
 821				Message left = lhs.getLatestMessage();
 822				Message right = rhs.getLatestMessage();
 823				if (left.getTimeSent() > right.getTimeSent()) {
 824					return -1;
 825				} else if (left.getTimeSent() < right.getTimeSent()) {
 826					return 1;
 827				} else {
 828					return 0;
 829				}
 830			}
 831		});
 832	}
 833
 834	public int loadMoreMessages(Conversation conversation, long timestamp) {
 835		List<Message> messages = databaseBackend.getMessages(conversation, 50,
 836				timestamp);
 837		for (Message message : messages) {
 838			message.setConversation(conversation);
 839		}
 840		conversation.getMessages().addAll(0, messages);
 841		return messages.size();
 842	}
 843
 844	public List<Account> getAccounts() {
 845		return this.accounts;
 846	}
 847
 848	public Conversation find(List<Conversation> haystack, Contact contact) {
 849		for (Conversation conversation : haystack) {
 850			if (conversation.getContact() == contact) {
 851				return conversation;
 852			}
 853		}
 854		return null;
 855	}
 856
 857	public Conversation find(List<Conversation> haystack, Account account,
 858			String jid) {
 859		for (Conversation conversation : haystack) {
 860			if ((account == null || conversation.getAccount().equals(account))
 861					&& (conversation.getContactJid().split("/", 2)[0]
 862							.equals(jid))) {
 863				return conversation;
 864			}
 865		}
 866		return null;
 867	}
 868
 869	public Conversation findOrCreateConversation(Account account, String jid,
 870			boolean muc) {
 871		Conversation conversation = find(account, jid);
 872		if (conversation != null) {
 873			return conversation;
 874		}
 875		conversation = databaseBackend.findConversation(account, jid);
 876		if (conversation != null) {
 877			conversation.setStatus(Conversation.STATUS_AVAILABLE);
 878			conversation.setAccount(account);
 879			if (muc) {
 880				conversation.setMode(Conversation.MODE_MULTI);
 881			} else {
 882				conversation.setMode(Conversation.MODE_SINGLE);
 883			}
 884			conversation.setMessages(databaseBackend.getMessages(conversation,
 885					50));
 886			this.databaseBackend.updateConversation(conversation);
 887		} else {
 888			String conversationName;
 889			Contact contact = account.getRoster().getContact(jid);
 890			if (contact != null) {
 891				conversationName = contact.getDisplayName();
 892			} else {
 893				conversationName = jid.split("@")[0];
 894			}
 895			if (muc) {
 896				conversation = new Conversation(conversationName, account, jid,
 897						Conversation.MODE_MULTI);
 898			} else {
 899				conversation = new Conversation(conversationName, account, jid,
 900						Conversation.MODE_SINGLE);
 901			}
 902			this.databaseBackend.createConversation(conversation);
 903		}
 904		this.conversations.add(conversation);
 905		updateConversationUi();
 906		return conversation;
 907	}
 908
 909	public void archiveConversation(Conversation conversation) {
 910		if (conversation.getMode() == Conversation.MODE_MULTI) {
 911			if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
 912				Bookmark bookmark = conversation.getBookmark();
 913				if (bookmark != null && bookmark.autojoin()) {
 914					bookmark.setAutojoin(false);
 915					pushBookmarks(bookmark.getAccount());
 916				}
 917			}
 918			leaveMuc(conversation);
 919		} else {
 920			conversation.endOtrIfNeeded();
 921		}
 922		this.databaseBackend.updateConversation(conversation);
 923		this.conversations.remove(conversation);
 924		updateConversationUi();
 925	}
 926
 927	public void clearConversationHistory(Conversation conversation) {
 928		this.databaseBackend.deleteMessagesInConversation(conversation);
 929		this.fileBackend.removeFiles(conversation);
 930		conversation.getMessages().clear();
 931		updateConversationUi();
 932	}
 933
 934	public int getConversationCount() {
 935		return this.databaseBackend.getConversationCount();
 936	}
 937
 938	public void createAccount(Account account) {
 939		databaseBackend.createAccount(account);
 940		this.accounts.add(account);
 941		this.reconnectAccount(account, false);
 942		updateAccountUi();
 943	}
 944
 945	public void updateAccount(Account account) {
 946		this.statusListener.onStatusChanged(account);
 947		databaseBackend.updateAccount(account);
 948		reconnectAccount(account, false);
 949		updateAccountUi();
 950		UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
 951	}
 952
 953	public void deleteAccount(Account account) {
 954		for (Conversation conversation : conversations) {
 955			if (conversation.getAccount() == account) {
 956				if (conversation.getMode() == Conversation.MODE_MULTI) {
 957					leaveMuc(conversation);
 958				} else if (conversation.getMode() == Conversation.MODE_SINGLE) {
 959					conversation.endOtrIfNeeded();
 960				}
 961				conversations.remove(conversation);
 962			}
 963		}
 964		if (account.getXmppConnection() != null) {
 965			this.disconnect(account, true);
 966		}
 967		databaseBackend.deleteAccount(account);
 968		this.accounts.remove(account);
 969		updateAccountUi();
 970		UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
 971	}
 972
 973	public void setOnConversationListChangedListener(
 974			OnConversationUpdate listener) {
 975		this.mNotificationService.deactivateGracePeriod();
 976		if (checkListeners()) {
 977			switchToForeground();
 978		}
 979		this.mOnConversationUpdate = listener;
 980		this.mNotificationService.setIsInForeground(true);
 981		this.convChangedListenerCount++;
 982	}
 983
 984	public void removeOnConversationListChangedListener() {
 985		this.convChangedListenerCount--;
 986		if (this.convChangedListenerCount == 0) {
 987			this.mOnConversationUpdate = null;
 988			this.mNotificationService.setIsInForeground(false);
 989			if (checkListeners()) {
 990				switchToBackground();
 991			}
 992		}
 993	}
 994
 995	public void setOnAccountListChangedListener(OnAccountUpdate listener) {
 996		this.mNotificationService.deactivateGracePeriod();
 997		if (checkListeners()) {
 998			switchToForeground();
 999		}
1000		this.mOnAccountUpdate = listener;
1001		this.accountChangedListenerCount++;
1002	}
1003
1004	public void removeOnAccountListChangedListener() {
1005		this.accountChangedListenerCount--;
1006		if (this.accountChangedListenerCount == 0) {
1007			this.mOnAccountUpdate = null;
1008			if (checkListeners()) {
1009				switchToBackground();
1010			}
1011		}
1012	}
1013
1014	public void setOnRosterUpdateListener(OnRosterUpdate listener) {
1015		this.mNotificationService.deactivateGracePeriod();
1016		if (checkListeners()) {
1017			switchToForeground();
1018		}
1019		this.mOnRosterUpdate = listener;
1020		this.rosterChangedListenerCount++;
1021	}
1022
1023	public void removeOnRosterUpdateListener() {
1024		this.rosterChangedListenerCount--;
1025		if (this.rosterChangedListenerCount == 0) {
1026			this.mOnRosterUpdate = null;
1027			if (checkListeners()) {
1028				switchToBackground();
1029			}
1030		}
1031	}
1032
1033	private boolean checkListeners() {
1034		return (this.mOnAccountUpdate == null
1035				&& this.mOnConversationUpdate == null && this.mOnRosterUpdate == null);
1036	}
1037
1038	private void switchToForeground() {
1039		for (Account account : getAccounts()) {
1040			if (account.getStatus() == Account.STATUS_ONLINE) {
1041				XmppConnection connection = account.getXmppConnection();
1042				if (connection != null && connection.getFeatures().csi()) {
1043					connection.sendActive();
1044					Log.d(Config.LOGTAG, account.getJid()
1045							+ " sending csi//active");
1046				}
1047			}
1048		}
1049	}
1050
1051	private void switchToBackground() {
1052		for (Account account : getAccounts()) {
1053			if (account.getStatus() == Account.STATUS_ONLINE) {
1054				XmppConnection connection = account.getXmppConnection();
1055				if (connection != null && connection.getFeatures().csi()) {
1056					connection.sendInactive();
1057					Log.d(Config.LOGTAG, account.getJid()
1058							+ " sending csi//inactive");
1059				}
1060			}
1061		}
1062	}
1063
1064	public void connectMultiModeConversations(Account account) {
1065		List<Conversation> conversations = getConversations();
1066		for (int i = 0; i < conversations.size(); i++) {
1067			Conversation conversation = conversations.get(i);
1068			if ((conversation.getMode() == Conversation.MODE_MULTI)
1069					&& (conversation.getAccount() == account)) {
1070				joinMuc(conversation);
1071			}
1072		}
1073	}
1074
1075	public void joinMuc(Conversation conversation) {
1076		Account account = conversation.getAccount();
1077		account.pendingConferenceJoins.remove(conversation);
1078		account.pendingConferenceLeaves.remove(conversation);
1079		if (account.getStatus() == Account.STATUS_ONLINE) {
1080			Log.d(Config.LOGTAG,
1081					"joining conversation " + conversation.getContactJid());
1082			String nick = conversation.getMucOptions().getProposedNick();
1083			conversation.getMucOptions().setJoinNick(nick);
1084			PresencePacket packet = new PresencePacket();
1085			String joinJid = conversation.getMucOptions().getJoinJid();
1086			packet.setAttribute("to", conversation.getMucOptions().getJoinJid());
1087			Element x = new Element("x");
1088			x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
1089			if (conversation.getMucOptions().getPassword() != null) {
1090				Element password = x.addChild("password");
1091				password.setContent(conversation.getMucOptions().getPassword());
1092			}
1093			String sig = account.getPgpSignature();
1094			if (sig != null) {
1095				packet.addChild("status").setContent("online");
1096				packet.addChild("x", "jabber:x:signed").setContent(sig);
1097			}
1098			if (conversation.getMessages().size() != 0) {
1099				final SimpleDateFormat mDateFormat = new SimpleDateFormat(
1100						"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
1101				mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
1102				Date date = new Date(conversation.getLatestMessage()
1103						.getTimeSent() + 1000);
1104				x.addChild("history").setAttribute("since",
1105						mDateFormat.format(date));
1106			}
1107			packet.addChild(x);
1108			sendPresencePacket(account, packet);
1109			if (!joinJid.equals(conversation.getContactJid())) {
1110				conversation.setContactJid(joinJid);
1111				databaseBackend.updateConversation(conversation);
1112			}
1113		} else {
1114			account.pendingConferenceJoins.add(conversation);
1115		}
1116	}
1117
1118	private OnRenameListener renameListener = null;
1119	private IqGenerator mIqGenerator = new IqGenerator(this);
1120
1121	public void setOnRenameListener(OnRenameListener listener) {
1122		this.renameListener = listener;
1123	}
1124
1125	public void providePasswordForMuc(Conversation conversation, String password) {
1126		if (conversation.getMode() == Conversation.MODE_MULTI) {
1127			conversation.getMucOptions().setPassword(password);
1128			if (conversation.getBookmark() != null) {
1129				conversation.getBookmark().setAutojoin(true);
1130				pushBookmarks(conversation.getAccount());
1131			}
1132			databaseBackend.updateConversation(conversation);
1133			joinMuc(conversation);
1134		}
1135	}
1136
1137	public void renameInMuc(final Conversation conversation, final String nick) {
1138		final MucOptions options = conversation.getMucOptions();
1139		options.setJoinNick(nick);
1140		if (options.online()) {
1141			Account account = conversation.getAccount();
1142			options.setOnRenameListener(new OnRenameListener() {
1143
1144				@Override
1145				public void onRename(boolean success) {
1146					if (renameListener != null) {
1147						renameListener.onRename(success);
1148					}
1149					if (success) {
1150						conversation.setContactJid(conversation.getMucOptions()
1151								.getJoinJid());
1152						databaseBackend.updateConversation(conversation);
1153						Bookmark bookmark = conversation.getBookmark();
1154						if (bookmark != null) {
1155							bookmark.setNick(nick);
1156							pushBookmarks(bookmark.getAccount());
1157						}
1158					}
1159				}
1160			});
1161			options.flagAboutToRename();
1162			PresencePacket packet = new PresencePacket();
1163			packet.setAttribute("to", options.getJoinJid());
1164			packet.setAttribute("from", conversation.getAccount().getFullJid());
1165
1166			String sig = account.getPgpSignature();
1167			if (sig != null) {
1168				packet.addChild("status").setContent("online");
1169				packet.addChild("x", "jabber:x:signed").setContent(sig);
1170			}
1171			sendPresencePacket(account, packet);
1172		} else {
1173			conversation.setContactJid(options.getJoinJid());
1174			databaseBackend.updateConversation(conversation);
1175			if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
1176				Bookmark bookmark = conversation.getBookmark();
1177				if (bookmark != null) {
1178					bookmark.setNick(nick);
1179					pushBookmarks(bookmark.getAccount());
1180				}
1181				joinMuc(conversation);
1182			}
1183		}
1184	}
1185
1186	public void leaveMuc(Conversation conversation) {
1187		Account account = conversation.getAccount();
1188		account.pendingConferenceJoins.remove(conversation);
1189		account.pendingConferenceLeaves.remove(conversation);
1190		if (account.getStatus() == Account.STATUS_ONLINE) {
1191			PresencePacket packet = new PresencePacket();
1192			packet.setAttribute("to", conversation.getMucOptions().getJoinJid());
1193			packet.setAttribute("from", conversation.getAccount().getFullJid());
1194			packet.setAttribute("type", "unavailable");
1195			sendPresencePacket(conversation.getAccount(), packet);
1196			conversation.getMucOptions().setOffline();
1197			conversation.deregisterWithBookmark();
1198			Log.d(Config.LOGTAG, conversation.getAccount().getJid()
1199					+ " leaving muc " + conversation.getContactJid());
1200		} else {
1201			account.pendingConferenceLeaves.add(conversation);
1202		}
1203	}
1204
1205	public void disconnect(Account account, boolean force) {
1206		if ((account.getStatus() == Account.STATUS_ONLINE)
1207				|| (account.getStatus() == Account.STATUS_DISABLED)) {
1208			if (!force) {
1209				List<Conversation> conversations = getConversations();
1210				for (int i = 0; i < conversations.size(); i++) {
1211					Conversation conversation = conversations.get(i);
1212					if (conversation.getAccount() == account) {
1213						if (conversation.getMode() == Conversation.MODE_MULTI) {
1214							leaveMuc(conversation);
1215						} else {
1216							conversation.endOtrIfNeeded();
1217						}
1218					}
1219				}
1220			}
1221			account.getXmppConnection().disconnect(force);
1222		}
1223	}
1224
1225	@Override
1226	public IBinder onBind(Intent intent) {
1227		return mBinder;
1228	}
1229
1230	public void updateMessage(Message message) {
1231		databaseBackend.updateMessage(message);
1232	}
1233
1234	protected void syncDirtyContacts(Account account) {
1235		for (Contact contact : account.getRoster().getContacts()) {
1236			if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
1237				pushContactToServer(contact);
1238			}
1239			if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
1240				deleteContactOnServer(contact);
1241			}
1242		}
1243	}
1244
1245	public void createContact(Contact contact) {
1246		SharedPreferences sharedPref = getPreferences();
1247		boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1248		if (autoGrant) {
1249			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
1250			contact.setOption(Contact.Options.ASKING);
1251		}
1252		pushContactToServer(contact);
1253	}
1254
1255	public void onOtrSessionEstablished(Conversation conversation) {
1256		Account account = conversation.getAccount();
1257		List<Message> messages = conversation.getMessages();
1258		Session otrSession = conversation.getOtrSession();
1259		Log.d(Config.LOGTAG,
1260				account.getJid() + " otr session established with "
1261						+ conversation.getContactJid() + "/"
1262						+ otrSession.getSessionID().getUserID());
1263		for (int i = 0; i < messages.size(); ++i) {
1264			Message msg = messages.get(i);
1265			if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING)
1266					&& (msg.getEncryption() == Message.ENCRYPTION_OTR)) {
1267				msg.setPresence(otrSession.getSessionID().getUserID());
1268				if (msg.getType() == Message.TYPE_TEXT) {
1269					MessagePacket outPacket = mMessageGenerator
1270							.generateOtrChat(msg, true);
1271					if (outPacket != null) {
1272						msg.setStatus(Message.STATUS_SEND);
1273						databaseBackend.updateMessage(msg);
1274						sendMessagePacket(account, outPacket);
1275					}
1276				} else if (msg.getType() == Message.TYPE_IMAGE) {
1277					mJingleConnectionManager.createNewConnection(msg);
1278				}
1279			}
1280		}
1281		updateConversationUi();
1282	}
1283
1284	public boolean renewSymmetricKey(Conversation conversation) {
1285		Account account = conversation.getAccount();
1286		byte[] symmetricKey = new byte[32];
1287		this.mRandom.nextBytes(symmetricKey);
1288		Session otrSession = conversation.getOtrSession();
1289		if (otrSession != null) {
1290			MessagePacket packet = new MessagePacket();
1291			packet.setType(MessagePacket.TYPE_CHAT);
1292			packet.setFrom(account.getFullJid());
1293			packet.addChild("private", "urn:xmpp:carbons:2");
1294			packet.addChild("no-copy", "urn:xmpp:hints");
1295			packet.setTo(otrSession.getSessionID().getAccountID() + "/"
1296					+ otrSession.getSessionID().getUserID());
1297			try {
1298				packet.setBody(otrSession
1299						.transformSending(CryptoHelper.FILETRANSFER
1300								+ CryptoHelper.bytesToHex(symmetricKey)));
1301				sendMessagePacket(account, packet);
1302				conversation.setSymmetricKey(symmetricKey);
1303				return true;
1304			} catch (OtrException e) {
1305				return false;
1306			}
1307		}
1308		return false;
1309	}
1310
1311	public void pushContactToServer(Contact contact) {
1312		contact.resetOption(Contact.Options.DIRTY_DELETE);
1313		contact.setOption(Contact.Options.DIRTY_PUSH);
1314		Account account = contact.getAccount();
1315		if (account.getStatus() == Account.STATUS_ONLINE) {
1316			boolean ask = contact.getOption(Contact.Options.ASKING);
1317			boolean sendUpdates = contact
1318					.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
1319					&& contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
1320			IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1321			iq.query("jabber:iq:roster").addChild(contact.asElement());
1322			account.getXmppConnection().sendIqPacket(iq, null);
1323			if (sendUpdates) {
1324				sendPresencePacket(account,
1325						mPresenceGenerator.sendPresenceUpdatesTo(contact));
1326			}
1327			if (ask) {
1328				sendPresencePacket(account,
1329						mPresenceGenerator.requestPresenceUpdatesFrom(contact));
1330			}
1331		}
1332	}
1333
1334	public void publishAvatar(Account account, Uri image,
1335			final UiCallback<Avatar> callback) {
1336		final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
1337		final int size = Config.AVATAR_SIZE;
1338		final Avatar avatar = getFileBackend()
1339				.getPepAvatar(image, size, format);
1340		if (avatar != null) {
1341			avatar.height = size;
1342			avatar.width = size;
1343			if (format.equals(Bitmap.CompressFormat.WEBP)) {
1344				avatar.type = "image/webp";
1345			} else if (format.equals(Bitmap.CompressFormat.JPEG)) {
1346				avatar.type = "image/jpeg";
1347			} else if (format.equals(Bitmap.CompressFormat.PNG)) {
1348				avatar.type = "image/png";
1349			}
1350			if (!getFileBackend().save(avatar)) {
1351				callback.error(R.string.error_saving_avatar, avatar);
1352				return;
1353			}
1354			IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
1355			this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1356
1357				@Override
1358				public void onIqPacketReceived(Account account, IqPacket result) {
1359					if (result.getType() == IqPacket.TYPE_RESULT) {
1360						IqPacket packet = XmppConnectionService.this.mIqGenerator
1361								.publishAvatarMetadata(avatar);
1362						sendIqPacket(account, packet, new OnIqPacketReceived() {
1363
1364							@Override
1365							public void onIqPacketReceived(Account account,
1366									IqPacket result) {
1367								if (result.getType() == IqPacket.TYPE_RESULT) {
1368									if (account.setAvatar(avatar.getFilename())) {
1369										databaseBackend.updateAccount(account);
1370									}
1371									callback.success(avatar);
1372								} else {
1373									callback.error(
1374											R.string.error_publish_avatar_server_reject,
1375											avatar);
1376								}
1377							}
1378						});
1379					} else {
1380						callback.error(
1381								R.string.error_publish_avatar_server_reject,
1382								avatar);
1383					}
1384				}
1385			});
1386		} else {
1387			callback.error(R.string.error_publish_avatar_converting, null);
1388		}
1389	}
1390
1391	public void fetchAvatar(Account account, Avatar avatar) {
1392		fetchAvatar(account, avatar, null);
1393	}
1394
1395	public void fetchAvatar(Account account, final Avatar avatar,
1396			final UiCallback<Avatar> callback) {
1397		IqPacket packet = this.mIqGenerator.retrieveAvatar(avatar);
1398		sendIqPacket(account, packet, new OnIqPacketReceived() {
1399
1400			@Override
1401			public void onIqPacketReceived(Account account, IqPacket result) {
1402				final String ERROR = account.getJid()
1403						+ ": fetching avatar for " + avatar.owner + " failed ";
1404				if (result.getType() == IqPacket.TYPE_RESULT) {
1405					avatar.image = mIqParser.avatarData(result);
1406					if (avatar.image != null) {
1407						if (getFileBackend().save(avatar)) {
1408							if (account.getJid().equals(avatar.owner)) {
1409								if (account.setAvatar(avatar.getFilename())) {
1410									databaseBackend.updateAccount(account);
1411								}
1412							} else {
1413								Contact contact = account.getRoster()
1414										.getContact(avatar.owner);
1415								contact.setAvatar(avatar.getFilename());
1416							}
1417							if (callback != null) {
1418								callback.success(avatar);
1419							}
1420							Log.d(Config.LOGTAG, account.getJid()
1421									+ ": succesfully fetched avatar for "
1422									+ avatar.owner);
1423							return;
1424						}
1425					} else {
1426
1427						Log.d(Config.LOGTAG, ERROR + "(parsing error)");
1428					}
1429				} else {
1430					Element error = result.findChild("error");
1431					if (error == null) {
1432						Log.d(Config.LOGTAG, ERROR + "(server error)");
1433					} else {
1434						Log.d(Config.LOGTAG, ERROR + error.toString());
1435					}
1436				}
1437				if (callback != null) {
1438					callback.error(0, null);
1439				}
1440
1441			}
1442		});
1443	}
1444
1445	public void checkForAvatar(Account account,
1446			final UiCallback<Avatar> callback) {
1447		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
1448		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1449
1450			@Override
1451			public void onIqPacketReceived(Account account, IqPacket packet) {
1452				if (packet.getType() == IqPacket.TYPE_RESULT) {
1453					Element pubsub = packet.findChild("pubsub",
1454							"http://jabber.org/protocol/pubsub");
1455					if (pubsub != null) {
1456						Element items = pubsub.findChild("items");
1457						if (items != null) {
1458							Avatar avatar = Avatar.parseMetadata(items);
1459							if (avatar != null) {
1460								avatar.owner = account.getJid();
1461								if (fileBackend.isAvatarCached(avatar)) {
1462									if (account.setAvatar(avatar.getFilename())) {
1463										databaseBackend.updateAccount(account);
1464									}
1465									callback.success(avatar);
1466								} else {
1467									fetchAvatar(account, avatar, callback);
1468								}
1469								return;
1470							}
1471						}
1472					}
1473				}
1474				callback.error(0, null);
1475			}
1476		});
1477	}
1478
1479	public void deleteContactOnServer(Contact contact) {
1480		contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
1481		contact.resetOption(Contact.Options.DIRTY_PUSH);
1482		contact.setOption(Contact.Options.DIRTY_DELETE);
1483		Account account = contact.getAccount();
1484		if (account.getStatus() == Account.STATUS_ONLINE) {
1485			IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1486			Element item = iq.query("jabber:iq:roster").addChild("item");
1487			item.setAttribute("jid", contact.getJid());
1488			item.setAttribute("subscription", "remove");
1489			account.getXmppConnection().sendIqPacket(iq, null);
1490		}
1491	}
1492
1493	public void updateConversation(Conversation conversation) {
1494		this.databaseBackend.updateConversation(conversation);
1495	}
1496
1497	public void reconnectAccount(final Account account, final boolean force) {
1498		new Thread(new Runnable() {
1499
1500			@Override
1501			public void run() {
1502				if (account.getXmppConnection() != null) {
1503					disconnect(account, force);
1504				}
1505				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1506					if (account.getXmppConnection() == null) {
1507						account.setXmppConnection(createConnection(account));
1508					}
1509					Thread thread = new Thread(account.getXmppConnection());
1510					thread.start();
1511					scheduleWakeupCall((int) (Config.CONNECT_TIMEOUT * 1.2),
1512							false);
1513				} else {
1514					account.getRoster().clearPresences();
1515					account.setXmppConnection(null);
1516				}
1517			}
1518		}).start();
1519	}
1520
1521	public void invite(Conversation conversation, String contact) {
1522		MessagePacket packet = mMessageGenerator.invite(conversation, contact);
1523		sendMessagePacket(conversation.getAccount(), packet);
1524	}
1525
1526	public void resetSendingToWaiting(Account account) {
1527		for (Conversation conversation : getConversations()) {
1528			if (conversation.getAccount() == account) {
1529				for (Message message : conversation.getMessages()) {
1530					if (message.getType() != Message.TYPE_IMAGE
1531							&& message.getStatus() == Message.STATUS_UNSEND) {
1532						markMessage(message, Message.STATUS_WAITING);
1533					}
1534				}
1535			}
1536		}
1537	}
1538
1539	public boolean markMessage(Account account, String recipient, String uuid,
1540			int status) {
1541		if (uuid == null) {
1542			return false;
1543		} else {
1544			for (Conversation conversation : getConversations()) {
1545				if (conversation.getContactJid().equals(recipient)
1546						&& conversation.getAccount().equals(account)) {
1547					return markMessage(conversation, uuid, status);
1548				}
1549			}
1550			return false;
1551		}
1552	}
1553
1554	public boolean markMessage(Conversation conversation, String uuid,
1555			int status) {
1556		if (uuid == null) {
1557			return false;
1558		} else {
1559			for (Message message : conversation.getMessages()) {
1560				if (uuid.equals(message.getUuid())
1561						|| (message.getStatus() >= Message.STATUS_SEND && uuid
1562								.equals(message.getRemoteMsgId()))) {
1563					markMessage(message, status);
1564					return true;
1565				}
1566			}
1567			return false;
1568		}
1569	}
1570
1571	public void markMessage(Message message, int status) {
1572		if (status == Message.STATUS_SEND_FAILED
1573				&& (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
1574						.getStatus() == Message.STATUS_SEND_DISPLAYED)) {
1575			return;
1576		}
1577		message.setStatus(status);
1578		databaseBackend.updateMessage(message);
1579		updateConversationUi();
1580	}
1581
1582	public SharedPreferences getPreferences() {
1583		return PreferenceManager
1584				.getDefaultSharedPreferences(getApplicationContext());
1585	}
1586
1587	public boolean forceEncryption() {
1588		return getPreferences().getBoolean("force_encryption", false);
1589	}
1590
1591	public boolean confirmMessages() {
1592		return getPreferences().getBoolean("confirm_messages", true);
1593	}
1594
1595	public boolean saveEncryptedMessages() {
1596		return !getPreferences().getBoolean("dont_save_encrypted", false);
1597	}
1598
1599	public boolean indicateReceived() {
1600		return getPreferences().getBoolean("indicate_received", false);
1601	}
1602
1603	public void updateConversationUi() {
1604		if (mOnConversationUpdate != null) {
1605			mOnConversationUpdate.onConversationUpdate();
1606		}
1607	}
1608
1609	public void updateAccountUi() {
1610		if (mOnAccountUpdate != null) {
1611			mOnAccountUpdate.onAccountUpdate();
1612		}
1613	}
1614
1615	public void updateRosterUi() {
1616		if (mOnRosterUpdate != null) {
1617			mOnRosterUpdate.onRosterUpdate();
1618		}
1619	}
1620
1621	public Account findAccountByJid(String accountJid) {
1622		for (Account account : this.accounts) {
1623			if (account.getJid().equals(accountJid)) {
1624				return account;
1625			}
1626		}
1627		return null;
1628	}
1629
1630	public Conversation findConversationByUuid(String uuid) {
1631		for (Conversation conversation : getConversations()) {
1632			if (conversation.getUuid().equals(uuid)) {
1633				return conversation;
1634			}
1635		}
1636		return null;
1637	}
1638
1639	public void markRead(Conversation conversation, boolean calledByUi) {
1640		mNotificationService.clear(conversation);
1641		String id = conversation.getLatestMarkableMessageId();
1642		conversation.markRead();
1643		if (confirmMessages() && id != null && calledByUi) {
1644			Log.d(Config.LOGTAG, conversation.getAccount().getJid()
1645					+ ": sending read marker for " + conversation.getName());
1646			Account account = conversation.getAccount();
1647			String to = conversation.getContactJid();
1648			this.sendMessagePacket(conversation.getAccount(),
1649					mMessageGenerator.confirm(account, to, id));
1650		}
1651		if (!calledByUi) {
1652			updateConversationUi();
1653		}
1654	}
1655
1656	public void failWaitingOtrMessages(Conversation conversation) {
1657		for (Message message : conversation.getMessages()) {
1658			if (message.getEncryption() == Message.ENCRYPTION_OTR
1659					&& message.getStatus() == Message.STATUS_WAITING) {
1660				markMessage(message, Message.STATUS_SEND_FAILED);
1661			}
1662		}
1663	}
1664
1665	public SecureRandom getRNG() {
1666		return this.mRandom;
1667	}
1668
1669	public MemorizingTrustManager getMemorizingTrustManager() {
1670		return this.mMemorizingTrustManager;
1671	}
1672
1673	public PowerManager getPowerManager() {
1674		return this.pm;
1675	}
1676
1677	public void replyWithNotAcceptable(Account account, MessagePacket packet) {
1678		if (account.getStatus() == Account.STATUS_ONLINE) {
1679			MessagePacket error = this.mMessageGenerator
1680					.generateNotAcceptable(packet);
1681			sendMessagePacket(account, error);
1682		}
1683	}
1684
1685	public void syncRosterToDisk(final Account account) {
1686		new Thread(new Runnable() {
1687
1688			@Override
1689			public void run() {
1690				databaseBackend.writeRoster(account.getRoster());
1691			}
1692		}).start();
1693
1694	}
1695
1696	public List<String> getKnownHosts() {
1697		List<String> hosts = new ArrayList<String>();
1698		for (Account account : getAccounts()) {
1699			if (!hosts.contains(account.getServer())) {
1700				hosts.add(account.getServer());
1701			}
1702			for (Contact contact : account.getRoster().getContacts()) {
1703				if (contact.showInRoster()) {
1704					String server = contact.getServer();
1705					if (server != null && !hosts.contains(server)) {
1706						hosts.add(server);
1707					}
1708				}
1709			}
1710		}
1711		return hosts;
1712	}
1713
1714	public List<String> getKnownConferenceHosts() {
1715		ArrayList<String> mucServers = new ArrayList<String>();
1716		for (Account account : accounts) {
1717			if (account.getXmppConnection() != null) {
1718				String server = account.getXmppConnection().getMucServer();
1719				if (server != null && !mucServers.contains(server)) {
1720					mucServers.add(server);
1721				}
1722			}
1723		}
1724		return mucServers;
1725	}
1726
1727	public void sendMessagePacket(Account account, MessagePacket packet) {
1728		XmppConnection connection = account.getXmppConnection();
1729		if (connection != null) {
1730			connection.sendMessagePacket(packet);
1731		}
1732	}
1733
1734	public void sendPresencePacket(Account account, PresencePacket packet) {
1735		XmppConnection connection = account.getXmppConnection();
1736		if (connection != null) {
1737			connection.sendPresencePacket(packet);
1738		}
1739	}
1740
1741	public void sendIqPacket(Account account, IqPacket packet,
1742			OnIqPacketReceived callback) {
1743		XmppConnection connection = account.getXmppConnection();
1744		if (connection != null) {
1745			connection.sendIqPacket(packet, callback);
1746		}
1747	}
1748
1749	public MessageGenerator getMessageGenerator() {
1750		return this.mMessageGenerator;
1751	}
1752
1753	public PresenceGenerator getPresenceGenerator() {
1754		return this.mPresenceGenerator;
1755	}
1756
1757	public IqGenerator getIqGenerator() {
1758		return this.mIqGenerator;
1759	}
1760
1761	public JingleConnectionManager getJingleConnectionManager() {
1762		return this.mJingleConnectionManager;
1763	}
1764
1765	public interface OnConversationUpdate {
1766		public void onConversationUpdate();
1767	}
1768
1769	public interface OnAccountUpdate {
1770		public void onAccountUpdate();
1771	}
1772
1773	public interface OnRosterUpdate {
1774		public void onRosterUpdate();
1775	}
1776
1777	public List<Contact> findContacts(String jid) {
1778		ArrayList<Contact> contacts = new ArrayList<Contact>();
1779		for (Account account : getAccounts()) {
1780			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1781				Contact contact = account.getRoster()
1782						.getContactAsShownInRoster(jid);
1783				if (contact != null) {
1784					contacts.add(contact);
1785				}
1786			}
1787		}
1788		return contacts;
1789	}
1790
1791	public NotificationService getNotificationService() {
1792		return this.mNotificationService;
1793	}
1794}