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