XmppConnectionService.java

   1package eu.siacs.conversations.services;
   2
   3import java.security.SecureRandom;
   4import java.util.Collections;
   5import java.util.Comparator;
   6import java.util.Hashtable;
   7import java.util.List;
   8import java.util.Locale;
   9
  10import org.openintents.openpgp.util.OpenPgpApi;
  11import org.openintents.openpgp.util.OpenPgpServiceConnection;
  12
  13import net.java.otr4j.OtrException;
  14import net.java.otr4j.session.Session;
  15import net.java.otr4j.session.SessionStatus;
  16import eu.siacs.conversations.crypto.PgpEngine;
  17import eu.siacs.conversations.entities.Account;
  18import eu.siacs.conversations.entities.Contact;
  19import eu.siacs.conversations.entities.Conversation;
  20import eu.siacs.conversations.entities.Message;
  21import eu.siacs.conversations.entities.MucOptions;
  22import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
  23import eu.siacs.conversations.entities.Presences;
  24import eu.siacs.conversations.parser.MessageParser;
  25import eu.siacs.conversations.parser.PresenceParser;
  26import eu.siacs.conversations.persistance.DatabaseBackend;
  27import eu.siacs.conversations.persistance.FileBackend;
  28import eu.siacs.conversations.ui.OnAccountListChangedListener;
  29import eu.siacs.conversations.ui.OnConversationListChangedListener;
  30import eu.siacs.conversations.ui.UiCallback;
  31import eu.siacs.conversations.utils.CryptoHelper;
  32import eu.siacs.conversations.utils.ExceptionHelper;
  33import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
  34import eu.siacs.conversations.utils.PRNGFixes;
  35import eu.siacs.conversations.utils.PhoneHelper;
  36import eu.siacs.conversations.utils.UIHelper;
  37import eu.siacs.conversations.xml.Element;
  38import eu.siacs.conversations.xmpp.OnBindListener;
  39import eu.siacs.conversations.xmpp.OnContactStatusChanged;
  40import eu.siacs.conversations.xmpp.OnIqPacketReceived;
  41import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
  42import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
  43import eu.siacs.conversations.xmpp.OnStatusChanged;
  44import eu.siacs.conversations.xmpp.OnTLSExceptionReceived;
  45import eu.siacs.conversations.xmpp.XmppConnection;
  46import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
  47import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
  48import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
  49import eu.siacs.conversations.xmpp.stanzas.IqPacket;
  50import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
  51import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
  52import android.annotation.SuppressLint;
  53import android.app.AlarmManager;
  54import android.app.PendingIntent;
  55import android.app.Service;
  56import android.content.Context;
  57import android.content.Intent;
  58import android.content.SharedPreferences;
  59import android.database.ContentObserver;
  60import android.net.ConnectivityManager;
  61import android.net.NetworkInfo;
  62import android.net.Uri;
  63import android.os.Binder;
  64import android.os.Bundle;
  65import android.os.IBinder;
  66import android.os.PowerManager;
  67import android.os.PowerManager.WakeLock;
  68import android.os.SystemClock;
  69import android.preference.PreferenceManager;
  70import android.provider.ContactsContract;
  71import android.util.Log;
  72
  73public class XmppConnectionService extends Service {
  74
  75	protected static final String LOGTAG = "xmppService";
  76	public DatabaseBackend databaseBackend;
  77	private FileBackend fileBackend;
  78
  79	public long startDate;
  80
  81	private static final int PING_MAX_INTERVAL = 300;
  82	private static final int PING_MIN_INTERVAL = 10;
  83	private static final int PING_TIMEOUT = 5;
  84	private static final int CONNECT_TIMEOUT = 60;
  85	private static final long CARBON_GRACE_PERIOD = 60000L;
  86
  87	private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
  88
  89	private MessageParser mMessageParser = new MessageParser(this);
  90	private PresenceParser mPresenceParser = new PresenceParser(this);
  91
  92	private List<Account> accounts;
  93	private List<Conversation> conversations = null;
  94	private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
  95			this);
  96
  97	private OnConversationListChangedListener convChangedListener = null;
  98	private int convChangedListenerCount = 0;
  99	private OnAccountListChangedListener accountChangedListener = null;
 100	private OnTLSExceptionReceived tlsException = null;
 101	public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
 102
 103		@Override
 104		public void onContactStatusChanged(Contact contact, boolean online) {
 105			Conversation conversation = findActiveConversation(contact);
 106			if (conversation != null) {
 107				conversation.endOtrIfNeeded();
 108				if (online && (contact.getPresences().size() == 1)) {
 109					sendUnsendMessages(conversation);
 110				}
 111			}
 112		}
 113	};
 114
 115	public void setOnTLSExceptionReceivedListener(
 116			OnTLSExceptionReceived listener) {
 117		tlsException = listener;
 118	}
 119
 120	private SecureRandom mRandom;
 121
 122	private long lastCarbonMessageReceived = -CARBON_GRACE_PERIOD;
 123
 124	private ContentObserver contactObserver = new ContentObserver(null) {
 125		@Override
 126		public void onChange(boolean selfChange) {
 127			super.onChange(selfChange);
 128			Intent intent = new Intent(getApplicationContext(),
 129					XmppConnectionService.class);
 130			intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
 131			startService(intent);
 132		}
 133	};
 134
 135	private final IBinder mBinder = new XmppConnectionBinder();
 136	private OnMessagePacketReceived messageListener = new OnMessagePacketReceived() {
 137
 138		@Override
 139		public void onMessagePacketReceived(Account account,
 140				MessagePacket packet) {
 141			Message message = null;
 142			boolean notify = true;
 143			if (getPreferences().getBoolean(
 144					"notification_grace_period_after_carbon_received", true)) {
 145				notify = (SystemClock.elapsedRealtime() - lastCarbonMessageReceived) > CARBON_GRACE_PERIOD;
 146			}
 147
 148			if ((packet.getType() == MessagePacket.TYPE_CHAT)) {
 149				if ((packet.getBody() != null)
 150						&& (packet.getBody().startsWith("?OTR"))) {
 151					message = mMessageParser.parseOtrChat(packet, account);
 152					if (message != null) {
 153						message.markUnread();
 154					}
 155				} else if (packet.hasChild("body")) {
 156					message = mMessageParser.parseChat(packet, account);
 157					message.markUnread();
 158				} else if (packet.hasChild("received")
 159						|| (packet.hasChild("sent"))) {
 160					message = mMessageParser
 161							.parseCarbonMessage(packet, account);
 162					if (message != null) {
 163						if (message.getStatus() == Message.STATUS_SEND) {
 164							lastCarbonMessageReceived = SystemClock
 165									.elapsedRealtime();
 166							notify = false;
 167							message.getConversation().markRead();
 168						} else {
 169							message.markUnread();
 170						}
 171					}
 172				}
 173
 174			} else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) {
 175				message = mMessageParser.parseGroupchat(packet, account);
 176				if (message != null) {
 177					if (message.getStatus() == Message.STATUS_RECIEVED) {
 178						message.markUnread();
 179					} else {
 180						message.getConversation().markRead();
 181						notify = false;
 182					}
 183				}
 184			} else if (packet.getType() == MessagePacket.TYPE_ERROR) {
 185				mMessageParser.parseError(packet, account);
 186				return;
 187			} else if (packet.getType() == MessagePacket.TYPE_NORMAL) {
 188				mMessageParser.parseNormal(packet, account);
 189			}
 190			if ((message == null) || (message.getBody() == null)) {
 191				return;
 192			}
 193			if ((confirmMessages()) && ((packet.getId() != null))) {
 194				MessagePacket receivedPacket = new MessagePacket();
 195				receivedPacket.setType(MessagePacket.TYPE_NORMAL);
 196				receivedPacket.setTo(message.getCounterpart());
 197				receivedPacket.setFrom(account.getFullJid());
 198				if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
 199					Element received = receivedPacket.addChild("received",
 200							"urn:xmpp:chat-markers:0");
 201					received.setAttribute("id", packet.getId());
 202					account.getXmppConnection().sendMessagePacket(
 203							receivedPacket);
 204				} else if (packet.hasChild("request", "urn:xmpp:receipts")) {
 205					Element received = receivedPacket.addChild("received",
 206							"urn:xmpp:receipts");
 207					received.setAttribute("id", packet.getId());
 208					account.getXmppConnection().sendMessagePacket(
 209							receivedPacket);
 210				}
 211			}
 212			Conversation conversation = message.getConversation();
 213			conversation.getMessages().add(message);
 214			if (packet.getType() != MessagePacket.TYPE_ERROR) {
 215				databaseBackend.createMessage(message);
 216			}
 217			if (convChangedListener != null) {
 218				convChangedListener.onConversationListChanged();
 219			} else {
 220				UIHelper.updateNotification(getApplicationContext(),
 221						getConversations(), message.getConversation(), notify);
 222			}
 223		}
 224	};
 225	private OnStatusChanged statusListener = new OnStatusChanged() {
 226
 227		@Override
 228		public void onStatusChanged(Account account) {
 229			if (accountChangedListener != null) {
 230				accountChangedListener.onAccountListChangedListener();
 231			}
 232			if (account.getStatus() == Account.STATUS_ONLINE) {
 233				List<Conversation> conversations = getConversations();
 234				for (int i = 0; i < conversations.size(); ++i) {
 235					if (conversations.get(i).getAccount() == account) {
 236						conversations.get(i).endOtrIfNeeded();
 237						sendUnsendMessages(conversations.get(i));
 238					}
 239				}
 240				syncDirtyContacts(account);
 241				scheduleWakeupCall(PING_MAX_INTERVAL, true);
 242			} else if (account.getStatus() == Account.STATUS_OFFLINE) {
 243				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 244					int timeToReconnect = mRandom.nextInt(50) + 10;
 245					scheduleWakeupCall(timeToReconnect, false);
 246				}
 247
 248			} else if (account.getStatus() == Account.STATUS_REGISTRATION_SUCCESSFULL) {
 249				databaseBackend.updateAccount(account);
 250				reconnectAccount(account, true);
 251			} else if ((account.getStatus() != Account.STATUS_CONNECTING)
 252					&& (account.getStatus() != Account.STATUS_NO_INTERNET)) {
 253				int next = account.getXmppConnection().getTimeToNextAttempt();
 254				Log.d(LOGTAG, account.getJid()
 255						+ ": error connecting account. try again in " + next
 256						+ "s for the "
 257						+ (account.getXmppConnection().getAttempt() + 1)
 258						+ " time");
 259				scheduleWakeupCall(next, false);
 260			}
 261			UIHelper.showErrorNotification(getApplicationContext(),
 262					getAccounts());
 263		}
 264	};
 265
 266	private OnPresencePacketReceived presenceListener = new OnPresencePacketReceived() {
 267
 268		@Override
 269		public void onPresencePacketReceived(final Account account,
 270				PresencePacket packet) {
 271			if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) {
 272				mPresenceParser.parseConferencePresence(packet, account);
 273			} else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
 274				mPresenceParser.parseConferencePresence(packet, account);
 275			} else {
 276				mPresenceParser.parseContactPresence(packet, account);
 277			}
 278		}
 279	};
 280
 281	private OnIqPacketReceived unknownIqListener = new OnIqPacketReceived() {
 282
 283		@Override
 284		public void onIqPacketReceived(Account account, IqPacket packet) {
 285			if (packet.hasChild("query", "jabber:iq:roster")) {
 286				String from = packet.getFrom();
 287				if ((from == null) || (from.equals(account.getJid()))) {
 288					Element query = packet.findChild("query");
 289					processRosterItems(account, query);
 290				} else {
 291					Log.d(LOGTAG, "unauthorized roster push from: " + from);
 292				}
 293			} else if (packet
 294					.hasChild("open", "http://jabber.org/protocol/ibb")
 295					|| packet
 296							.hasChild("data", "http://jabber.org/protocol/ibb")) {
 297				XmppConnectionService.this.mJingleConnectionManager
 298						.deliverIbbPacket(account, packet);
 299			} else if (packet.hasChild("query",
 300					"http://jabber.org/protocol/disco#info")) {
 301				IqPacket iqResponse = packet
 302						.generateRespone(IqPacket.TYPE_RESULT);
 303				Element query = iqResponse.addChild("query",
 304						"http://jabber.org/protocol/disco#info");
 305				query.addChild("feature").setAttribute("var",
 306						"urn:xmpp:jingle:1");
 307				query.addChild("feature").setAttribute("var",
 308						"urn:xmpp:jingle:apps:file-transfer:3");
 309				query.addChild("feature").setAttribute("var",
 310						"urn:xmpp:jingle:transports:s5b:1");
 311				query.addChild("feature").setAttribute("var",
 312						"urn:xmpp:jingle:transports:ibb:1");
 313				if (confirmMessages()) {
 314					query.addChild("feature").setAttribute("var",
 315							"urn:xmpp:receipts");
 316				}
 317				account.getXmppConnection().sendIqPacket(iqResponse, null);
 318			} else {
 319				if ((packet.getType() == IqPacket.TYPE_GET)
 320						|| (packet.getType() == IqPacket.TYPE_SET)) {
 321					IqPacket response = packet
 322							.generateRespone(IqPacket.TYPE_ERROR);
 323					Element error = response.addChild("error");
 324					error.setAttribute("type", "cancel");
 325					error.addChild("feature-not-implemented",
 326							"urn:ietf:params:xml:ns:xmpp-stanzas");
 327					account.getXmppConnection().sendIqPacket(response, null);
 328				}
 329			}
 330		}
 331	};
 332
 333	private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
 334
 335		@Override
 336		public void onJinglePacketReceived(Account account, JinglePacket packet) {
 337			mJingleConnectionManager.deliverPacket(account, packet);
 338		}
 339	};
 340
 341	private OpenPgpServiceConnection pgpServiceConnection;
 342	private PgpEngine mPgpEngine = null;
 343	private Intent pingIntent;
 344	private PendingIntent pendingPingIntent = null;
 345	private WakeLock wakeLock;
 346	private PowerManager pm;
 347
 348	public PgpEngine getPgpEngine() {
 349		if (pgpServiceConnection.isBound()) {
 350			if (this.mPgpEngine == null) {
 351				this.mPgpEngine = new PgpEngine(new OpenPgpApi(
 352						getApplicationContext(),
 353						pgpServiceConnection.getService()), this);
 354			}
 355			return mPgpEngine;
 356		} else {
 357			return null;
 358		}
 359
 360	}
 361
 362	public FileBackend getFileBackend() {
 363		return this.fileBackend;
 364	}
 365
 366	public Message attachImageToConversation(final Conversation conversation,
 367			final Uri uri, final UiCallback<Message> callback) {
 368		final Message message;
 369		if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 370			message = new Message(conversation, "",
 371					Message.ENCRYPTION_DECRYPTED);
 372		} else {
 373			message = new Message(conversation, "", conversation.getNextEncryption());
 374		}
 375		message.setPresence(conversation.getNextPresence());
 376		message.setType(Message.TYPE_IMAGE);
 377		message.setStatus(Message.STATUS_OFFERED);
 378		new Thread(new Runnable() {
 379
 380			@Override
 381			public void run() {
 382				try {
 383					getFileBackend().copyImageToPrivateStorage(message, uri);
 384					if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 385						getPgpEngine().encrypt(message, callback);
 386					} else {
 387						callback.success(message);
 388					}
 389				} catch (FileBackend.ImageCopyException e) {
 390					callback.error(e.getResId(), message);
 391				}
 392			}
 393		}).start();
 394		return message;
 395	}
 396
 397	public Conversation findMuc(String name, Account account) {
 398		for (Conversation conversation : this.conversations) {
 399			if (conversation.getContactJid().split("/")[0].equals(name)
 400					&& (conversation.getAccount() == account)) {
 401				return conversation;
 402			}
 403		}
 404		return null;
 405	}
 406
 407	private void processRosterItems(Account account, Element elements) {
 408		String version = elements.getAttribute("ver");
 409		if (version != null) {
 410			account.getRoster().setVersion(version);
 411		}
 412		for (Element item : elements.getChildren()) {
 413			if (item.getName().equals("item")) {
 414				String jid = item.getAttribute("jid");
 415				String name = item.getAttribute("name");
 416				String subscription = item.getAttribute("subscription");
 417				Contact contact = account.getRoster().getContact(jid);
 418				if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
 419					contact.setServerName(name);
 420				}
 421				if (subscription.equals("remove")) {
 422					contact.resetOption(Contact.Options.IN_ROSTER);
 423					contact.resetOption(Contact.Options.DIRTY_DELETE);
 424				} else {
 425					contact.setOption(Contact.Options.IN_ROSTER);
 426					contact.parseSubscriptionFromElement(item);
 427				}
 428			}
 429		}
 430	}
 431
 432	public class XmppConnectionBinder extends Binder {
 433		public XmppConnectionService getService() {
 434			return XmppConnectionService.this;
 435		}
 436	}
 437
 438	@Override
 439	public int onStartCommand(Intent intent, int flags, int startId) {
 440		if ((intent != null)
 441				&& (ACTION_MERGE_PHONE_CONTACTS.equals(intent.getAction()))) {
 442			mergePhoneContactsWithRoster();
 443			return START_STICKY;
 444		} else if ((intent != null)
 445				&& (Intent.ACTION_SHUTDOWN.equals(intent.getAction()))) {
 446			logoutAndSave();
 447			return START_NOT_STICKY;
 448		}
 449		this.wakeLock.acquire();
 450		ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
 451				.getSystemService(Context.CONNECTIVITY_SERVICE);
 452		NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
 453		boolean isConnected = activeNetwork != null
 454				&& activeNetwork.isConnected();
 455
 456		for (Account account : accounts) {
 457			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 458				if (!isConnected) {
 459					account.setStatus(Account.STATUS_NO_INTERNET);
 460					if (statusListener != null) {
 461						statusListener.onStatusChanged(account);
 462					}
 463				} else {
 464					if (account.getStatus() == Account.STATUS_NO_INTERNET) {
 465						account.setStatus(Account.STATUS_OFFLINE);
 466						if (statusListener != null) {
 467							statusListener.onStatusChanged(account);
 468						}
 469					}
 470					if (account.getStatus() == Account.STATUS_ONLINE) {
 471						long lastReceived = account.getXmppConnection().lastPaketReceived;
 472						long lastSent = account.getXmppConnection().lastPingSent;
 473						if (lastSent - lastReceived >= PING_TIMEOUT * 1000) {
 474							Log.d(LOGTAG, account.getJid() + ": ping timeout");
 475							this.reconnectAccount(account, true);
 476						} else if (SystemClock.elapsedRealtime() - lastReceived >= PING_MIN_INTERVAL * 1000) {
 477							account.getXmppConnection().sendPing();
 478							account.getXmppConnection().lastPingSent = SystemClock
 479									.elapsedRealtime();
 480							this.scheduleWakeupCall(2, false);
 481						}
 482					} else if (account.getStatus() == Account.STATUS_OFFLINE) {
 483						if (account.getXmppConnection() == null) {
 484							account.setXmppConnection(this
 485									.createConnection(account));
 486						}
 487						account.getXmppConnection().lastPingSent = SystemClock
 488								.elapsedRealtime();
 489						new Thread(account.getXmppConnection()).start();
 490					} else if ((account.getStatus() == Account.STATUS_CONNECTING)
 491							&& ((SystemClock.elapsedRealtime() - account
 492									.getXmppConnection().lastConnect) / 1000 >= CONNECT_TIMEOUT)) {
 493						Log.d(LOGTAG, account.getJid()
 494								+ ": time out during connect reconnecting");
 495						reconnectAccount(account, true);
 496					} else {
 497						if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
 498							reconnectAccount(account, true);
 499						}
 500					}
 501					// in any case. reschedule wakup call
 502					this.scheduleWakeupCall(PING_MAX_INTERVAL, true);
 503				}
 504				if (accountChangedListener != null) {
 505					accountChangedListener.onAccountListChangedListener();
 506				}
 507			}
 508		}
 509		if (wakeLock.isHeld()) {
 510			wakeLock.release();
 511		}
 512		return START_STICKY;
 513	}
 514
 515	@SuppressLint("TrulyRandom")
 516	@Override
 517	public void onCreate() {
 518		ExceptionHelper.init(getApplicationContext());
 519		PRNGFixes.apply();
 520		this.mRandom = new SecureRandom();
 521		this.databaseBackend = DatabaseBackend
 522				.getInstance(getApplicationContext());
 523		this.fileBackend = new FileBackend(getApplicationContext());
 524		this.accounts = databaseBackend.getAccounts();
 525
 526		for (Account account : this.accounts) {
 527			this.databaseBackend.readRoster(account.getRoster());
 528		}
 529		this.mergePhoneContactsWithRoster();
 530		this.getConversations();
 531
 532		getContentResolver().registerContentObserver(
 533				ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
 534		this.pgpServiceConnection = new OpenPgpServiceConnection(
 535				getApplicationContext(), "org.sufficientlysecure.keychain");
 536		this.pgpServiceConnection.bindToService();
 537
 538		this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 539		this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
 540				"XmppConnectionService");
 541	}
 542
 543	@Override
 544	public void onDestroy() {
 545		super.onDestroy();
 546		this.logoutAndSave();
 547	}
 548
 549	@Override
 550	public void onTaskRemoved(Intent rootIntent) {
 551		super.onTaskRemoved(rootIntent);
 552		this.logoutAndSave();
 553	}
 554
 555	private void logoutAndSave() {
 556		for (Account account : accounts) {
 557			databaseBackend.writeRoster(account.getRoster());
 558			if (account.getXmppConnection() != null) {
 559				disconnect(account, false);
 560			}
 561		}
 562		Context context = getApplicationContext();
 563		AlarmManager alarmManager = (AlarmManager) context
 564				.getSystemService(Context.ALARM_SERVICE);
 565		Intent intent = new Intent(context, EventReceiver.class);
 566		alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
 567		Log.d(LOGTAG, "good bye");
 568		stopSelf();
 569	}
 570
 571	protected void scheduleWakeupCall(int seconds, boolean ping) {
 572		long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000;
 573		Context context = getApplicationContext();
 574		AlarmManager alarmManager = (AlarmManager) context
 575				.getSystemService(Context.ALARM_SERVICE);
 576
 577		if (ping) {
 578			if (this.pingIntent == null) {
 579				this.pingIntent = new Intent(context, EventReceiver.class);
 580				this.pingIntent.setAction("ping");
 581				this.pingIntent.putExtra("time", timeToWake);
 582				this.pendingPingIntent = PendingIntent.getBroadcast(context, 0,
 583						this.pingIntent, 0);
 584				alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
 585						timeToWake, pendingPingIntent);
 586			} else {
 587				long scheduledTime = this.pingIntent.getLongExtra("time", 0);
 588				if (scheduledTime < SystemClock.elapsedRealtime()
 589						|| (scheduledTime > timeToWake)) {
 590					this.pingIntent.putExtra("time", timeToWake);
 591					alarmManager.cancel(this.pendingPingIntent);
 592					this.pendingPingIntent = PendingIntent.getBroadcast(
 593							context, 0, this.pingIntent, 0);
 594					alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
 595							timeToWake, pendingPingIntent);
 596				}
 597			}
 598		} else {
 599			Intent intent = new Intent(context, EventReceiver.class);
 600			intent.setAction("ping_check");
 601			PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0,
 602					intent, 0);
 603			alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake,
 604					alarmIntent);
 605		}
 606
 607	}
 608
 609	public XmppConnection createConnection(Account account) {
 610		SharedPreferences sharedPref = getPreferences();
 611		account.setResource(sharedPref.getString("resource", "mobile")
 612				.toLowerCase(Locale.getDefault()));
 613		XmppConnection connection = new XmppConnection(account, this);
 614		connection.setOnMessagePacketReceivedListener(this.messageListener);
 615		connection.setOnStatusChangedListener(this.statusListener);
 616		connection.setOnPresencePacketReceivedListener(this.presenceListener);
 617		connection
 618				.setOnUnregisteredIqPacketReceivedListener(this.unknownIqListener);
 619		connection.setOnJinglePacketReceivedListener(this.jingleListener);
 620		connection
 621				.setOnTLSExceptionReceivedListener(new OnTLSExceptionReceived() {
 622
 623					@Override
 624					public void onTLSExceptionReceived(String fingerprint,
 625							Account account) {
 626						Log.d(LOGTAG, "tls exception arrived in service");
 627						if (tlsException != null) {
 628							tlsException.onTLSExceptionReceived(fingerprint,
 629									account);
 630						}
 631					}
 632				});
 633		connection.setOnBindListener(new OnBindListener() {
 634
 635			@Override
 636			public void onBind(final Account account) {
 637				account.getRoster().clearPresences();
 638				account.clearPresences(); // self presences
 639				fetchRosterFromServer(account);
 640				sendPresence(account);
 641				connectMultiModeConversations(account);
 642				if (convChangedListener != null) {
 643					convChangedListener.onConversationListChanged();
 644				}
 645			}
 646		});
 647		return connection;
 648	}
 649
 650	synchronized public void sendMessage(Message message) {
 651		Account account = message.getConversation().getAccount();
 652		Conversation conv = message.getConversation();
 653		MessagePacket packet = null;
 654		boolean saveInDb = true;
 655		boolean send = false;
 656		if (account.getStatus() == Account.STATUS_ONLINE) {
 657			if (message.getType() == Message.TYPE_IMAGE) {
 658				if (message.getPresence() != null) {
 659					mJingleConnectionManager.createNewConnection(message);
 660				} else {
 661					message.setStatus(Message.STATUS_WAITING);
 662				}
 663			} else {
 664				if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 665					if (!conv.hasValidOtrSession()
 666							&& (message.getPresence() != null)) {
 667						// starting otr session. messages will be send later
 668						conv.startOtrSession(getApplicationContext(),
 669								message.getPresence(), true);
 670					} else if (conv.hasValidOtrSession()
 671							&& conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 672						// otr session aleary exists, creating message packet
 673						// accordingly
 674						packet = prepareMessagePacket(account, message,
 675								conv.getOtrSession());
 676						send = true;
 677						message.setStatus(Message.STATUS_SEND);
 678					} else if (message.getPresence() == null) {
 679						message.setStatus(Message.STATUS_WAITING);
 680					}
 681				} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 682					message.getConversation().endOtrIfNeeded();
 683					packet = prepareMessagePacket(account, message, null);
 684					packet.setBody("This is an XEP-0027 encryted message");
 685					packet.addChild("x", "jabber:x:encrypted").setContent(
 686							message.getEncryptedBody());
 687					message.setStatus(Message.STATUS_SEND);
 688					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 689					send = true;
 690				} else {
 691					message.getConversation().endOtrIfNeeded();
 692					// don't encrypt
 693					if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
 694						message.setStatus(Message.STATUS_SEND);
 695					}
 696					packet = prepareMessagePacket(account, message, null);
 697					send = true;
 698				}
 699			}
 700		} else {
 701			message.setStatus(Message.STATUS_WAITING);
 702			if (message.getType() == Message.TYPE_TEXT) {
 703				if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 704					String pgpBody = message.getEncryptedBody();
 705					String decryptedBody = message.getBody();
 706					message.setBody(pgpBody);
 707					databaseBackend.createMessage(message);
 708					saveInDb = false;
 709					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 710					message.setBody(decryptedBody);
 711				} else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 712					if (conv.hasValidOtrSession()) {
 713						message.setPresence(conv.getOtrSession().getSessionID()
 714								.getUserID());
 715					} else if (!conv.hasValidOtrSession()
 716							&& message.getPresence() != null) {
 717						conv.startOtrSession(getApplicationContext(),
 718								message.getPresence(), false);
 719					}
 720				}
 721			}
 722
 723		}
 724		if (saveInDb) {
 725			databaseBackend.createMessage(message);
 726		}
 727		conv.getMessages().add(message);
 728		if (convChangedListener != null) {
 729			convChangedListener.onConversationListChanged();
 730		}
 731		if ((send) && (packet != null)) {
 732			account.getXmppConnection().sendMessagePacket(packet);
 733		}
 734
 735	}
 736
 737	private void sendUnsendMessages(Conversation conversation) {
 738		for (int i = 0; i < conversation.getMessages().size(); ++i) {
 739			int status = conversation.getMessages().get(i).getStatus();
 740			if (status == Message.STATUS_WAITING) {
 741				resendMessage(conversation.getMessages().get(i));
 742			}
 743		}
 744	}
 745
 746	private void resendMessage(Message message) {
 747		Account account = message.getConversation().getAccount();
 748		if (message.getType() == Message.TYPE_TEXT) {
 749			MessagePacket packet = null;
 750			if (message.getEncryption() == Message.ENCRYPTION_NONE) {
 751				packet = prepareMessagePacket(account, message, null);
 752			} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 753				packet = prepareMessagePacket(account, message, null);
 754				packet.setBody("This is an XEP-0027 encryted message");
 755				if (message.getEncryptedBody() == null) {
 756					markMessage(message, Message.STATUS_SEND_FAILED);
 757					return;
 758				}
 759				packet.addChild("x", "jabber:x:encrypted").setContent(
 760						message.getEncryptedBody());
 761			} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 762				packet = prepareMessagePacket(account, message, null);
 763				packet.setBody("This is an XEP-0027 encryted message");
 764				packet.addChild("x", "jabber:x:encrypted").setContent(
 765						message.getBody());
 766			} else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 767				Presences presences = message.getConversation().getContact()
 768						.getPresences();
 769				if (!message.getConversation().hasValidOtrSession()) {
 770					if ((message.getPresence() != null)
 771							&& (presences.has(message.getPresence()))) {
 772						message.getConversation().startOtrSession(
 773								getApplicationContext(), message.getPresence(),
 774								true);
 775					} else {
 776						if (presences.size() == 1) {
 777							String presence = presences.asStringArray()[0];
 778							message.getConversation().startOtrSession(
 779									getApplicationContext(), presence, true);
 780						}
 781					}
 782				}
 783			}
 784			if (packet != null) {
 785				account.getXmppConnection().sendMessagePacket(packet);
 786				markMessage(message, Message.STATUS_SEND);
 787			}
 788		} else if (message.getType() == Message.TYPE_IMAGE) {
 789			Presences presences = message.getConversation().getContact()
 790					.getPresences();
 791			if ((message.getPresence() != null)
 792					&& (presences.has(message.getPresence()))) {
 793				markMessage(message, Message.STATUS_OFFERED);
 794				mJingleConnectionManager.createNewConnection(message);
 795			} else {
 796				if (presences.size() == 1) {
 797					String presence = presences.asStringArray()[0];
 798					message.setPresence(presence);
 799					markMessage(message, Message.STATUS_OFFERED);
 800					mJingleConnectionManager.createNewConnection(message);
 801				}
 802			}
 803		}
 804	}
 805
 806	public MessagePacket prepareMessagePacket(Account account, Message message,
 807			Session otrSession) {
 808		MessagePacket packet = new MessagePacket();
 809		if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
 810			packet.setType(MessagePacket.TYPE_CHAT);
 811			packet.setFrom(account.getFullJid());
 812			if (otrSession != null) {
 813				try {
 814					packet.setBody(otrSession.transformSending(message
 815							.getBody()));
 816				} catch (OtrException e) {
 817					Log.d(LOGTAG,
 818							account.getJid()
 819									+ ": could not encrypt message to "
 820									+ message.getCounterpart());
 821				}
 822				packet.addChild("private", "urn:xmpp:carbons:2");
 823				packet.addChild("no-copy", "urn:xmpp:hints");
 824				packet.setTo(otrSession.getSessionID().getAccountID() + "/"
 825						+ otrSession.getSessionID().getUserID());
 826			} else {
 827				packet.setBody(message.getBody());
 828				packet.setTo(message.getCounterpart());
 829			}
 830			packet.addChild("markable", "urn:xmpp:chat-markers:0");
 831		} else if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
 832			packet.setType(MessagePacket.TYPE_GROUPCHAT);
 833			packet.setBody(message.getBody());
 834			packet.setTo(message.getCounterpart().split("/")[0]);
 835			packet.setFrom(account.getJid());
 836		}
 837		packet.setId(message.getUuid());
 838		return packet;
 839	}
 840
 841	public void fetchRosterFromServer(Account account) {
 842		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
 843		if (!"".equals(account.getRosterVersion())) {
 844			Log.d(LOGTAG, account.getJid() + ": fetching roster version "
 845					+ account.getRosterVersion());
 846		} else {
 847			Log.d(LOGTAG, account.getJid() + ": fetching roster");
 848		}
 849		iqPacket.query("jabber:iq:roster").setAttribute("ver",
 850				account.getRosterVersion());
 851		account.getXmppConnection().sendIqPacket(iqPacket,
 852				new OnIqPacketReceived() {
 853
 854					@Override
 855					public void onIqPacketReceived(final Account account,
 856							IqPacket packet) {
 857						Element roster = packet.findChild("query");
 858						if (roster != null) {
 859							account.getRoster().markAllAsNotInRoster();
 860							processRosterItems(account, roster);
 861						}
 862					}
 863				});
 864	}
 865
 866	private void mergePhoneContactsWithRoster() {
 867		PhoneHelper.loadPhoneContacts(getApplicationContext(),
 868				new OnPhoneContactsLoadedListener() {
 869					@Override
 870					public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
 871						for (Account account : accounts) {
 872							account.getRoster().clearSystemAccounts();
 873						}
 874						for (Bundle phoneContact : phoneContacts) {
 875							for (Account account : accounts) {
 876								String jid = phoneContact.getString("jid");
 877								Contact contact = account.getRoster()
 878										.getContact(jid);
 879								String systemAccount = phoneContact
 880										.getInt("phoneid")
 881										+ "#"
 882										+ phoneContact.getString("lookup");
 883								contact.setSystemAccount(systemAccount);
 884								contact.setPhotoUri(phoneContact
 885										.getString("photouri"));
 886								contact.setSystemName(phoneContact
 887										.getString("displayname"));
 888							}
 889						}
 890					}
 891				});
 892	}
 893
 894	public List<Conversation> getConversations() {
 895		if (this.conversations == null) {
 896			Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
 897			for (Account account : this.accounts) {
 898				accountLookupTable.put(account.getUuid(), account);
 899			}
 900			this.conversations = databaseBackend
 901					.getConversations(Conversation.STATUS_AVAILABLE);
 902			for (Conversation conv : this.conversations) {
 903				Account account = accountLookupTable.get(conv.getAccountUuid());
 904				conv.setAccount(account);
 905				conv.setMessages(databaseBackend.getMessages(conv, 50));
 906			}
 907		}
 908		Collections.sort(this.conversations, new Comparator<Conversation>() {
 909			@Override
 910			public int compare(Conversation lhs, Conversation rhs) {
 911				Message left = lhs.getLatestMessage();
 912				Message right = rhs.getLatestMessage();
 913				if (left.getTimeSent() > right.getTimeSent()) {
 914					return -1;
 915				} else if (left.getTimeSent() < right.getTimeSent()) {
 916					return 1;
 917				} else {
 918					return 0;
 919				}
 920			}
 921		});
 922		return this.conversations;
 923	}
 924
 925	public List<Message> getMoreMessages(Conversation conversation,
 926			long timestamp) {
 927		List<Message> messages = databaseBackend.getMessages(conversation, 50,
 928				timestamp);
 929		for (Message message : messages) {
 930			message.setConversation(conversation);
 931		}
 932		return messages;
 933	}
 934
 935	public List<Account> getAccounts() {
 936		return this.accounts;
 937	}
 938
 939	public Conversation findActiveConversation(Contact contact) {
 940		for (Conversation conversation : this.getConversations()) {
 941			if (conversation.getContact() == contact) {
 942				return conversation;
 943			}
 944		}
 945		return null;
 946	}
 947
 948	public Conversation findOrCreateConversation(Account account, String jid,
 949			boolean muc) {
 950		for (Conversation conv : this.getConversations()) {
 951			if ((conv.getAccount().equals(account))
 952					&& (conv.getContactJid().split("/")[0].equals(jid))) {
 953				return conv;
 954			}
 955		}
 956		Conversation conversation = databaseBackend.findConversation(account,
 957				jid);
 958		if (conversation != null) {
 959			conversation.setStatus(Conversation.STATUS_AVAILABLE);
 960			conversation.setAccount(account);
 961			if (muc) {
 962				conversation.setMode(Conversation.MODE_MULTI);
 963			} else {
 964				conversation.setMode(Conversation.MODE_SINGLE);
 965			}
 966			conversation.setMessages(databaseBackend.getMessages(conversation,
 967					50));
 968			this.databaseBackend.updateConversation(conversation);
 969		} else {
 970			String conversationName;
 971			Contact contact = account.getRoster().getContact(jid);
 972			if (contact != null) {
 973				conversationName = contact.getDisplayName();
 974			} else {
 975				conversationName = jid.split("@")[0];
 976			}
 977			if (muc) {
 978				conversation = new Conversation(conversationName, account, jid,
 979						Conversation.MODE_MULTI);
 980			} else {
 981				conversation = new Conversation(conversationName, account, jid,
 982						Conversation.MODE_SINGLE);
 983			}
 984			this.databaseBackend.createConversation(conversation);
 985		}
 986		this.conversations.add(conversation);
 987		if ((account.getStatus() == Account.STATUS_ONLINE)
 988				&& (conversation.getMode() == Conversation.MODE_MULTI)) {
 989			joinMuc(conversation);
 990		}
 991		if (this.convChangedListener != null) {
 992			this.convChangedListener.onConversationListChanged();
 993		}
 994		return conversation;
 995	}
 996
 997	public void archiveConversation(Conversation conversation) {
 998		if (conversation.getMode() == Conversation.MODE_MULTI) {
 999			leaveMuc(conversation);
1000		} else {
1001			conversation.endOtrIfNeeded();
1002		}
1003		this.databaseBackend.updateConversation(conversation);
1004		this.conversations.remove(conversation);
1005		if (this.convChangedListener != null) {
1006			this.convChangedListener.onConversationListChanged();
1007		}
1008	}
1009
1010	public void clearConversationHistory(Conversation conversation) {
1011		this.databaseBackend.deleteMessagesInConversation(conversation);
1012		this.fileBackend.removeFiles(conversation);
1013		conversation.getMessages().clear();
1014		if (this.convChangedListener != null) {
1015			this.convChangedListener.onConversationListChanged();
1016		}
1017	}
1018
1019	public int getConversationCount() {
1020		return this.databaseBackend.getConversationCount();
1021	}
1022
1023	public void createAccount(Account account) {
1024		databaseBackend.createAccount(account);
1025		this.accounts.add(account);
1026		this.reconnectAccount(account, false);
1027		if (accountChangedListener != null)
1028			accountChangedListener.onAccountListChangedListener();
1029	}
1030
1031	public void updateAccount(Account account) {
1032		this.statusListener.onStatusChanged(account);
1033		databaseBackend.updateAccount(account);
1034		reconnectAccount(account, false);
1035		if (accountChangedListener != null) {
1036			accountChangedListener.onAccountListChangedListener();
1037		}
1038		UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
1039	}
1040
1041	public void deleteAccount(Account account) {
1042		if (account.getXmppConnection() != null) {
1043			this.disconnect(account, true);
1044		}
1045		databaseBackend.deleteAccount(account);
1046		this.accounts.remove(account);
1047		if (accountChangedListener != null) {
1048			accountChangedListener.onAccountListChangedListener();
1049		}
1050		UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
1051	}
1052
1053	public void setOnConversationListChangedListener(
1054			OnConversationListChangedListener listener) {
1055		this.convChangedListener = listener;
1056		this.convChangedListenerCount++;
1057	}
1058
1059	public void removeOnConversationListChangedListener() {
1060		this.convChangedListenerCount--;
1061		if (this.convChangedListenerCount == 0) {
1062			this.convChangedListener = null;
1063		}
1064	}
1065
1066	public void setOnAccountListChangedListener(
1067			OnAccountListChangedListener listener) {
1068		this.accountChangedListener = listener;
1069	}
1070
1071	public void removeOnAccountListChangedListener() {
1072		this.accountChangedListener = null;
1073	}
1074
1075	public void connectMultiModeConversations(Account account) {
1076		List<Conversation> conversations = getConversations();
1077		for (int i = 0; i < conversations.size(); i++) {
1078			Conversation conversation = conversations.get(i);
1079			if ((conversation.getMode() == Conversation.MODE_MULTI)
1080					&& (conversation.getAccount() == account)) {
1081				joinMuc(conversation);
1082			}
1083		}
1084	}
1085
1086	public void joinMuc(Conversation conversation) {
1087		Account account = conversation.getAccount();
1088		String[] mucParts = conversation.getContactJid().split("/");
1089		String muc;
1090		String nick;
1091		if (mucParts.length == 2) {
1092			muc = mucParts[0];
1093			nick = mucParts[1];
1094		} else {
1095			muc = mucParts[0];
1096			nick = account.getUsername();
1097		}
1098		PresencePacket packet = new PresencePacket();
1099		packet.setAttribute("to", muc + "/" + nick);
1100		Element x = new Element("x");
1101		x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
1102		String sig = account.getPgpSignature();
1103		if (sig != null) {
1104			packet.addChild("status").setContent("online");
1105			packet.addChild("x", "jabber:x:signed").setContent(sig);
1106		}
1107		if (conversation.getMessages().size() != 0) {
1108			long lastMsgTime = conversation.getLatestMessage().getTimeSent();
1109			long diff = (System.currentTimeMillis() - lastMsgTime) / 1000 - 1;
1110			x.addChild("history").setAttribute("seconds", diff + "");
1111		}
1112		packet.addChild(x);
1113		account.getXmppConnection().sendPresencePacket(packet);
1114	}
1115
1116	private OnRenameListener renameListener = null;
1117
1118	public void setOnRenameListener(OnRenameListener listener) {
1119		this.renameListener = listener;
1120	}
1121
1122	public void renameInMuc(final Conversation conversation, final String nick) {
1123		final MucOptions options = conversation.getMucOptions();
1124		if (options.online()) {
1125			Account account = conversation.getAccount();
1126			options.setOnRenameListener(new OnRenameListener() {
1127
1128				@Override
1129				public void onRename(boolean success) {
1130					if (renameListener != null) {
1131						renameListener.onRename(success);
1132					}
1133					if (success) {
1134						String jid = conversation.getContactJid().split("/")[0]
1135								+ "/" + nick;
1136						conversation.setContactJid(jid);
1137						databaseBackend.updateConversation(conversation);
1138					}
1139				}
1140			});
1141			options.flagAboutToRename();
1142			PresencePacket packet = new PresencePacket();
1143			packet.setAttribute("to",
1144					conversation.getContactJid().split("/")[0] + "/" + nick);
1145			packet.setAttribute("from", conversation.getAccount().getFullJid());
1146
1147			String sig = account.getPgpSignature();
1148			if (sig != null) {
1149				packet.addChild("status").setContent("online");
1150				packet.addChild("x", "jabber:x:signed").setContent(sig);
1151			}
1152
1153			account.getXmppConnection().sendPresencePacket(packet, null);
1154		} else {
1155			String jid = conversation.getContactJid().split("/")[0] + "/"
1156					+ nick;
1157			conversation.setContactJid(jid);
1158			databaseBackend.updateConversation(conversation);
1159			if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
1160				joinMuc(conversation);
1161			}
1162		}
1163	}
1164
1165	public void leaveMuc(Conversation conversation) {
1166		PresencePacket packet = new PresencePacket();
1167		packet.setAttribute("to", conversation.getContactJid().split("/")[0]
1168				+ "/" + conversation.getMucOptions().getNick());
1169		packet.setAttribute("from", conversation.getAccount().getFullJid());
1170		packet.setAttribute("type", "unavailable");
1171		Log.d(LOGTAG, "send leaving muc " + packet);
1172		conversation.getAccount().getXmppConnection()
1173				.sendPresencePacket(packet);
1174		conversation.getMucOptions().setOffline();
1175	}
1176
1177	public void disconnect(Account account, boolean force) {
1178		if ((account.getStatus() == Account.STATUS_ONLINE)
1179				|| (account.getStatus() == Account.STATUS_DISABLED)) {
1180			if (!force) {
1181				List<Conversation> conversations = getConversations();
1182				for (int i = 0; i < conversations.size(); i++) {
1183					Conversation conversation = conversations.get(i);
1184					if (conversation.getAccount() == account) {
1185						if (conversation.getMode() == Conversation.MODE_MULTI) {
1186							leaveMuc(conversation);
1187						} else {
1188							conversation.endOtrIfNeeded();
1189						}
1190					}
1191				}
1192			}
1193			account.getXmppConnection().disconnect(force);
1194		}
1195	}
1196
1197	@Override
1198	public IBinder onBind(Intent intent) {
1199		return mBinder;
1200	}
1201
1202	public void updateMessage(Message message) {
1203		databaseBackend.updateMessage(message);
1204	}
1205
1206	protected void syncDirtyContacts(Account account) {
1207		for (Contact contact : account.getRoster().getContacts()) {
1208			if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
1209				pushContactToServer(contact);
1210			}
1211			if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
1212				Log.d(LOGTAG, "dirty delete");
1213				deleteContactOnServer(contact);
1214			}
1215		}
1216	}
1217
1218	public void createContact(Contact contact) {
1219		SharedPreferences sharedPref = getPreferences();
1220		boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1221		if (autoGrant) {
1222			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
1223			contact.setOption(Contact.Options.ASKING);
1224		}
1225		pushContactToServer(contact);
1226	}
1227
1228	public void onOtrSessionEstablished(Conversation conversation) {
1229		Account account = conversation.getAccount();
1230		List<Message> messages = conversation.getMessages();
1231		Session otrSession = conversation.getOtrSession();
1232		Log.d(LOGTAG, account.getJid() + " otr session established with "
1233				+ conversation.getContactJid() + "/"
1234				+ otrSession.getSessionID().getUserID());
1235		for (int i = 0; i < messages.size(); ++i) {
1236			Message msg = messages.get(i);
1237			if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING)
1238					&& (msg.getEncryption() == Message.ENCRYPTION_OTR)) {
1239				MessagePacket outPacket = prepareMessagePacket(account, msg,
1240						otrSession);
1241				msg.setStatus(Message.STATUS_SEND);
1242				databaseBackend.updateMessage(msg);
1243				account.getXmppConnection().sendMessagePacket(outPacket);
1244			}
1245		}
1246		updateUi(conversation, false);
1247	}
1248	
1249	public boolean renewSymmetricKey(Conversation conversation) {
1250		Account account = conversation.getAccount();
1251		byte[] symmetricKey = new byte[32];
1252		this.mRandom.nextBytes(symmetricKey);
1253		Session otrSession = conversation.getOtrSession();
1254		if (otrSession!=null) {
1255			MessagePacket packet = new MessagePacket();
1256			packet.setType(MessagePacket.TYPE_CHAT);
1257			packet.setFrom(account.getFullJid());
1258			packet.addChild("private", "urn:xmpp:carbons:2");
1259			packet.addChild("no-copy", "urn:xmpp:hints");
1260			packet.setTo(otrSession.getSessionID().getAccountID() + "/"
1261					+ otrSession.getSessionID().getUserID());
1262			try {
1263				packet.setBody(otrSession.transformSending(CryptoHelper.FILETRANSFER+CryptoHelper.bytesToHex(symmetricKey)));
1264				account.getXmppConnection().sendMessagePacket(packet);
1265				conversation.setSymmetricKey(symmetricKey);
1266				return true;
1267			} catch (OtrException e) {
1268				return false;
1269			}
1270		}
1271		return false;
1272	}
1273
1274	public void pushContactToServer(Contact contact) {
1275		contact.resetOption(Contact.Options.DIRTY_DELETE);
1276		Account account = contact.getAccount();
1277		if (account.getStatus() == Account.STATUS_ONLINE) {
1278			IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1279			iq.query("jabber:iq:roster").addChild(contact.asElement());
1280			account.getXmppConnection().sendIqPacket(iq, null);
1281			if (contact.getOption(Contact.Options.ASKING)) {
1282				requestPresenceUpdatesFrom(contact);
1283			}
1284			if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1285				Log.d("xmppService", "contact had pending subscription");
1286				sendPresenceUpdatesTo(contact);
1287			}
1288			contact.resetOption(Contact.Options.DIRTY_PUSH);
1289		} else {
1290			contact.setOption(Contact.Options.DIRTY_PUSH);
1291		}
1292	}
1293
1294	public void deleteContactOnServer(Contact contact) {
1295		contact.resetOption(Contact.Options.DIRTY_PUSH);
1296		Account account = contact.getAccount();
1297		if (account.getStatus() == Account.STATUS_ONLINE) {
1298			IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1299			Element item = iq.query("jabber:iq:roster").addChild("item");
1300			item.setAttribute("jid", contact.getJid());
1301			item.setAttribute("subscription", "remove");
1302			account.getXmppConnection().sendIqPacket(iq, null);
1303			contact.resetOption(Contact.Options.DIRTY_DELETE);
1304		} else {
1305			contact.setOption(Contact.Options.DIRTY_DELETE);
1306		}
1307	}
1308
1309	public void requestPresenceUpdatesFrom(Contact contact) {
1310		PresencePacket packet = new PresencePacket();
1311		packet.setAttribute("type", "subscribe");
1312		packet.setAttribute("to", contact.getJid());
1313		packet.setAttribute("from", contact.getAccount().getJid());
1314		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1315	}
1316
1317	public void stopPresenceUpdatesFrom(Contact contact) {
1318		PresencePacket packet = new PresencePacket();
1319		packet.setAttribute("type", "unsubscribe");
1320		packet.setAttribute("to", contact.getJid());
1321		packet.setAttribute("from", contact.getAccount().getJid());
1322		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1323	}
1324
1325	public void stopPresenceUpdatesTo(Contact contact) {
1326		PresencePacket packet = new PresencePacket();
1327		packet.setAttribute("type", "unsubscribed");
1328		packet.setAttribute("to", contact.getJid());
1329		packet.setAttribute("from", contact.getAccount().getJid());
1330		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1331	}
1332
1333	public void sendPresenceUpdatesTo(Contact contact) {
1334		PresencePacket packet = new PresencePacket();
1335		packet.setAttribute("type", "subscribed");
1336		packet.setAttribute("to", contact.getJid());
1337		packet.setAttribute("from", contact.getAccount().getJid());
1338		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1339	}
1340
1341	public void sendPresence(Account account) {
1342		PresencePacket packet = new PresencePacket();
1343		packet.setAttribute("from", account.getFullJid());
1344		String sig = account.getPgpSignature();
1345		if (sig != null) {
1346			packet.addChild("status").setContent("online");
1347			packet.addChild("x", "jabber:x:signed").setContent(sig);
1348		}
1349		account.getXmppConnection().sendPresencePacket(packet);
1350	}
1351
1352	public void updateConversation(Conversation conversation) {
1353		this.databaseBackend.updateConversation(conversation);
1354	}
1355
1356	public void removeOnTLSExceptionReceivedListener() {
1357		this.tlsException = null;
1358	}
1359
1360	public void reconnectAccount(final Account account, final boolean force) {
1361		new Thread(new Runnable() {
1362
1363			@Override
1364			public void run() {
1365				if (account.getXmppConnection() != null) {
1366					disconnect(account, force);
1367				}
1368				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1369					if (account.getXmppConnection() == null) {
1370						account.setXmppConnection(createConnection(account));
1371					}
1372					Thread thread = new Thread(account.getXmppConnection());
1373					thread.start();
1374					scheduleWakeupCall((int) (CONNECT_TIMEOUT * 1.2), false);
1375				}
1376			}
1377		}).start();
1378	}
1379
1380	public void sendConversationSubject(Conversation conversation,
1381			String subject) {
1382		MessagePacket packet = new MessagePacket();
1383		packet.setType(MessagePacket.TYPE_GROUPCHAT);
1384		packet.setTo(conversation.getContactJid().split("/")[0]);
1385		Element subjectChild = new Element("subject");
1386		subjectChild.setContent(subject);
1387		packet.addChild(subjectChild);
1388		packet.setFrom(conversation.getAccount().getJid());
1389		Account account = conversation.getAccount();
1390		if (account.getStatus() == Account.STATUS_ONLINE) {
1391			account.getXmppConnection().sendMessagePacket(packet);
1392		}
1393	}
1394
1395	public void inviteToConference(Conversation conversation,
1396			List<Contact> contacts) {
1397		for (Contact contact : contacts) {
1398			MessagePacket packet = new MessagePacket();
1399			packet.setTo(conversation.getContactJid().split("/")[0]);
1400			packet.setFrom(conversation.getAccount().getFullJid());
1401			Element x = new Element("x");
1402			x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
1403			Element invite = new Element("invite");
1404			invite.setAttribute("to", contact.getJid());
1405			x.addChild(invite);
1406			packet.addChild(x);
1407			Log.d(LOGTAG, packet.toString());
1408			conversation.getAccount().getXmppConnection()
1409					.sendMessagePacket(packet);
1410		}
1411
1412	}
1413
1414	public boolean markMessage(Account account, String recipient, String uuid,
1415			int status) {
1416		for (Conversation conversation : getConversations()) {
1417			if (conversation.getContactJid().equals(recipient)
1418					&& conversation.getAccount().equals(account)) {
1419				return markMessage(conversation, uuid, status);
1420			}
1421		}
1422		return false;
1423	}
1424
1425	public boolean markMessage(Conversation conversation, String uuid,
1426			int status) {
1427		for (Message message : conversation.getMessages()) {
1428			if (message.getUuid().equals(uuid)) {
1429				markMessage(message, status);
1430				return true;
1431			}
1432		}
1433		return false;
1434	}
1435
1436	public void markMessage(Message message, int status) {
1437		message.setStatus(status);
1438		databaseBackend.updateMessage(message);
1439		if (convChangedListener != null) {
1440			convChangedListener.onConversationListChanged();
1441		}
1442	}
1443
1444	public SharedPreferences getPreferences() {
1445		return PreferenceManager
1446				.getDefaultSharedPreferences(getApplicationContext());
1447	}
1448
1449	public boolean confirmMessages() {
1450		return getPreferences().getBoolean("confirm_messages", true);
1451	}
1452
1453	public void updateUi(Conversation conversation, boolean notify) {
1454		if (convChangedListener != null) {
1455			convChangedListener.onConversationListChanged();
1456		} else {
1457			UIHelper.updateNotification(getApplicationContext(),
1458					getConversations(), conversation, notify);
1459		}
1460	}
1461
1462	public Account findAccountByJid(String accountJid) {
1463		for (Account account : this.accounts) {
1464			if (account.getJid().equals(accountJid)) {
1465				return account;
1466			}
1467		}
1468		return null;
1469	}
1470
1471	public void markRead(Conversation conversation) {
1472		conversation.markRead(this);
1473	}
1474
1475	public void sendConfirmMessage(Account account, String to, String id) {
1476		MessagePacket receivedPacket = new MessagePacket();
1477		receivedPacket.setType(MessagePacket.TYPE_NORMAL);
1478		receivedPacket.setTo(to);
1479		receivedPacket.setFrom(account.getFullJid());
1480		Element received = receivedPacket.addChild("displayed",
1481				"urn:xmpp:chat-markers:0");
1482		received.setAttribute("id", id);
1483		account.getXmppConnection().sendMessagePacket(receivedPacket);
1484	}
1485
1486	public SecureRandom getRNG() {
1487		return this.mRandom;
1488	}
1489
1490	public PowerManager getPowerManager() {
1491		return this.pm;
1492	}
1493}