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, "",
 374					conversation.getNextEncryption());
 375		}
 376		message.setPresence(conversation.getNextPresence());
 377		message.setType(Message.TYPE_IMAGE);
 378		message.setStatus(Message.STATUS_OFFERED);
 379		new Thread(new Runnable() {
 380
 381			@Override
 382			public void run() {
 383				try {
 384					getFileBackend().copyImageToPrivateStorage(message, uri);
 385					if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
 386						getPgpEngine().encrypt(message, callback);
 387					} else {
 388						callback.success(message);
 389					}
 390				} catch (FileBackend.ImageCopyException e) {
 391					callback.error(e.getResId(), message);
 392				}
 393			}
 394		}).start();
 395		return message;
 396	}
 397
 398	public Conversation findMuc(String name, Account account) {
 399		for (Conversation conversation : this.conversations) {
 400			if (conversation.getContactJid().split("/")[0].equals(name)
 401					&& (conversation.getAccount() == account)) {
 402				return conversation;
 403			}
 404		}
 405		return null;
 406	}
 407
 408	private void processRosterItems(Account account, Element elements) {
 409		String version = elements.getAttribute("ver");
 410		if (version != null) {
 411			account.getRoster().setVersion(version);
 412		}
 413		for (Element item : elements.getChildren()) {
 414			if (item.getName().equals("item")) {
 415				String jid = item.getAttribute("jid");
 416				String name = item.getAttribute("name");
 417				String subscription = item.getAttribute("subscription");
 418				Contact contact = account.getRoster().getContact(jid);
 419				if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
 420					contact.setServerName(name);
 421				}
 422				if (subscription.equals("remove")) {
 423					contact.resetOption(Contact.Options.IN_ROSTER);
 424					contact.resetOption(Contact.Options.DIRTY_DELETE);
 425				} else {
 426					contact.setOption(Contact.Options.IN_ROSTER);
 427					contact.parseSubscriptionFromElement(item);
 428				}
 429			}
 430		}
 431	}
 432
 433	public class XmppConnectionBinder extends Binder {
 434		public XmppConnectionService getService() {
 435			return XmppConnectionService.this;
 436		}
 437	}
 438
 439	@Override
 440	public int onStartCommand(Intent intent, int flags, int startId) {
 441		if ((intent != null)
 442				&& (ACTION_MERGE_PHONE_CONTACTS.equals(intent.getAction()))) {
 443			mergePhoneContactsWithRoster();
 444			return START_STICKY;
 445		} else if ((intent != null)
 446				&& (Intent.ACTION_SHUTDOWN.equals(intent.getAction()))) {
 447			logoutAndSave();
 448			return START_NOT_STICKY;
 449		}
 450		this.wakeLock.acquire();
 451		ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
 452				.getSystemService(Context.CONNECTIVITY_SERVICE);
 453		NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
 454		boolean isConnected = activeNetwork != null
 455				&& activeNetwork.isConnected();
 456
 457		for (Account account : accounts) {
 458			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 459				if (!isConnected) {
 460					account.setStatus(Account.STATUS_NO_INTERNET);
 461					if (statusListener != null) {
 462						statusListener.onStatusChanged(account);
 463					}
 464				} else {
 465					if (account.getStatus() == Account.STATUS_NO_INTERNET) {
 466						account.setStatus(Account.STATUS_OFFLINE);
 467						if (statusListener != null) {
 468							statusListener.onStatusChanged(account);
 469						}
 470					}
 471					if (account.getStatus() == Account.STATUS_ONLINE) {
 472						long lastReceived = account.getXmppConnection().lastPaketReceived;
 473						long lastSent = account.getXmppConnection().lastPingSent;
 474						if (lastSent - lastReceived >= PING_TIMEOUT * 1000) {
 475							Log.d(LOGTAG, account.getJid() + ": ping timeout");
 476							this.reconnectAccount(account, true);
 477						} else if (SystemClock.elapsedRealtime() - lastReceived >= PING_MIN_INTERVAL * 1000) {
 478							account.getXmppConnection().sendPing();
 479							account.getXmppConnection().lastPingSent = SystemClock
 480									.elapsedRealtime();
 481							this.scheduleWakeupCall(2, false);
 482						}
 483					} else if (account.getStatus() == Account.STATUS_OFFLINE) {
 484						if (account.getXmppConnection() == null) {
 485							account.setXmppConnection(this
 486									.createConnection(account));
 487						}
 488						account.getXmppConnection().lastPingSent = SystemClock
 489								.elapsedRealtime();
 490						new Thread(account.getXmppConnection()).start();
 491					} else if ((account.getStatus() == Account.STATUS_CONNECTING)
 492							&& ((SystemClock.elapsedRealtime() - account
 493									.getXmppConnection().lastConnect) / 1000 >= CONNECT_TIMEOUT)) {
 494						Log.d(LOGTAG, account.getJid()
 495								+ ": time out during connect reconnecting");
 496						reconnectAccount(account, true);
 497					} else {
 498						if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
 499							reconnectAccount(account, true);
 500						}
 501					}
 502					// in any case. reschedule wakup call
 503					this.scheduleWakeupCall(PING_MAX_INTERVAL, true);
 504				}
 505				if (accountChangedListener != null) {
 506					accountChangedListener.onAccountListChangedListener();
 507				}
 508			}
 509		}
 510		if (wakeLock.isHeld()) {
 511			wakeLock.release();
 512		}
 513		return START_STICKY;
 514	}
 515
 516	@SuppressLint("TrulyRandom")
 517	@Override
 518	public void onCreate() {
 519		ExceptionHelper.init(getApplicationContext());
 520		PRNGFixes.apply();
 521		this.mRandom = new SecureRandom();
 522		this.databaseBackend = DatabaseBackend
 523				.getInstance(getApplicationContext());
 524		this.fileBackend = new FileBackend(getApplicationContext());
 525		this.accounts = databaseBackend.getAccounts();
 526
 527		for (Account account : this.accounts) {
 528			this.databaseBackend.readRoster(account.getRoster());
 529		}
 530		this.mergePhoneContactsWithRoster();
 531		this.getConversations();
 532
 533		getContentResolver().registerContentObserver(
 534				ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
 535		this.pgpServiceConnection = new OpenPgpServiceConnection(
 536				getApplicationContext(), "org.sufficientlysecure.keychain");
 537		this.pgpServiceConnection.bindToService();
 538
 539		this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 540		this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
 541				"XmppConnectionService");
 542	}
 543
 544	@Override
 545	public void onDestroy() {
 546		super.onDestroy();
 547		this.logoutAndSave();
 548	}
 549
 550	@Override
 551	public void onTaskRemoved(Intent rootIntent) {
 552		super.onTaskRemoved(rootIntent);
 553		this.logoutAndSave();
 554	}
 555
 556	private void logoutAndSave() {
 557		for (Account account : accounts) {
 558			databaseBackend.writeRoster(account.getRoster());
 559			if (account.getXmppConnection() != null) {
 560				disconnect(account, false);
 561			}
 562		}
 563		Context context = getApplicationContext();
 564		AlarmManager alarmManager = (AlarmManager) context
 565				.getSystemService(Context.ALARM_SERVICE);
 566		Intent intent = new Intent(context, EventReceiver.class);
 567		alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
 568		Log.d(LOGTAG, "good bye");
 569		stopSelf();
 570	}
 571
 572	protected void scheduleWakeupCall(int seconds, boolean ping) {
 573		long timeToWake = SystemClock.elapsedRealtime() + seconds * 1000;
 574		Context context = getApplicationContext();
 575		AlarmManager alarmManager = (AlarmManager) context
 576				.getSystemService(Context.ALARM_SERVICE);
 577
 578		if (ping) {
 579			if (this.pingIntent == null) {
 580				this.pingIntent = new Intent(context, EventReceiver.class);
 581				this.pingIntent.setAction("ping");
 582				this.pingIntent.putExtra("time", timeToWake);
 583				this.pendingPingIntent = PendingIntent.getBroadcast(context, 0,
 584						this.pingIntent, 0);
 585				alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
 586						timeToWake, pendingPingIntent);
 587			} else {
 588				long scheduledTime = this.pingIntent.getLongExtra("time", 0);
 589				if (scheduledTime < SystemClock.elapsedRealtime()
 590						|| (scheduledTime > timeToWake)) {
 591					this.pingIntent.putExtra("time", timeToWake);
 592					alarmManager.cancel(this.pendingPingIntent);
 593					this.pendingPingIntent = PendingIntent.getBroadcast(
 594							context, 0, this.pingIntent, 0);
 595					alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP,
 596							timeToWake, pendingPingIntent);
 597				}
 598			}
 599		} else {
 600			Intent intent = new Intent(context, EventReceiver.class);
 601			intent.setAction("ping_check");
 602			PendingIntent alarmIntent = PendingIntent.getBroadcast(context, 0,
 603					intent, 0);
 604			alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake,
 605					alarmIntent);
 606		}
 607
 608	}
 609
 610	public XmppConnection createConnection(Account account) {
 611		SharedPreferences sharedPref = getPreferences();
 612		account.setResource(sharedPref.getString("resource", "mobile")
 613				.toLowerCase(Locale.getDefault()));
 614		XmppConnection connection = new XmppConnection(account, this);
 615		connection.setOnMessagePacketReceivedListener(this.messageListener);
 616		connection.setOnStatusChangedListener(this.statusListener);
 617		connection.setOnPresencePacketReceivedListener(this.presenceListener);
 618		connection
 619				.setOnUnregisteredIqPacketReceivedListener(this.unknownIqListener);
 620		connection.setOnJinglePacketReceivedListener(this.jingleListener);
 621		connection
 622				.setOnTLSExceptionReceivedListener(new OnTLSExceptionReceived() {
 623
 624					@Override
 625					public void onTLSExceptionReceived(String fingerprint,
 626							Account account) {
 627						Log.d(LOGTAG, "tls exception arrived in service");
 628						if (tlsException != null) {
 629							tlsException.onTLSExceptionReceived(fingerprint,
 630									account);
 631						}
 632					}
 633				});
 634		connection.setOnBindListener(new OnBindListener() {
 635
 636			@Override
 637			public void onBind(final Account account) {
 638				account.getRoster().clearPresences();
 639				account.clearPresences(); // self presences
 640				fetchRosterFromServer(account);
 641				sendPresence(account);
 642				connectMultiModeConversations(account);
 643				if (convChangedListener != null) {
 644					convChangedListener.onConversationListChanged();
 645				}
 646			}
 647		});
 648		return connection;
 649	}
 650
 651	synchronized public void sendMessage(Message message) {
 652		Account account = message.getConversation().getAccount();
 653		Conversation conv = message.getConversation();
 654		MessagePacket packet = null;
 655		boolean saveInDb = true;
 656		boolean send = false;
 657		if (account.getStatus() == Account.STATUS_ONLINE) {
 658			if (message.getType() == Message.TYPE_IMAGE) {
 659				if (message.getPresence() != null) {
 660					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 661						if (!conv.hasValidOtrSession()
 662								&& (message.getPresence() != null)) {
 663							conv.startOtrSession(getApplicationContext(),
 664									message.getPresence(), true);
 665							message.setStatus(Message.STATUS_WAITING);
 666						} else if (conv.hasValidOtrSession()
 667								&& conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 668							mJingleConnectionManager
 669									.createNewConnection(message);
 670						} else if (message.getPresence() == null) {
 671							message.setStatus(Message.STATUS_WAITING);
 672						}
 673					} else {
 674						mJingleConnectionManager.createNewConnection(message);
 675					}
 676				} else {
 677					message.setStatus(Message.STATUS_WAITING);
 678				}
 679			} else {
 680				if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 681					if (!conv.hasValidOtrSession()
 682							&& (message.getPresence() != null)) {
 683						conv.startOtrSession(getApplicationContext(),
 684								message.getPresence(), true);
 685						message.setStatus(Message.STATUS_WAITING);
 686					} else if (conv.hasValidOtrSession()
 687							&& conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 688						packet = prepareMessagePacket(account, message,
 689								conv.getOtrSession());
 690						send = true;
 691						message.setStatus(Message.STATUS_SEND);
 692					} else if (message.getPresence() == null) {
 693						message.setStatus(Message.STATUS_WAITING);
 694					}
 695				} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 696					message.getConversation().endOtrIfNeeded();
 697					packet = prepareMessagePacket(account, message, null);
 698					packet.setBody("This is an XEP-0027 encryted message");
 699					packet.addChild("x", "jabber:x:encrypted").setContent(
 700							message.getEncryptedBody());
 701					message.setStatus(Message.STATUS_SEND);
 702					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 703					send = true;
 704				} else {
 705					message.getConversation().endOtrIfNeeded();
 706					// don't encrypt
 707					if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
 708						message.setStatus(Message.STATUS_SEND);
 709					}
 710					packet = prepareMessagePacket(account, message, null);
 711					send = true;
 712				}
 713			}
 714		} else {
 715			message.setStatus(Message.STATUS_WAITING);
 716			if (message.getType() == Message.TYPE_TEXT) {
 717				if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 718					String pgpBody = message.getEncryptedBody();
 719					String decryptedBody = message.getBody();
 720					message.setBody(pgpBody);
 721					databaseBackend.createMessage(message);
 722					saveInDb = false;
 723					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 724					message.setBody(decryptedBody);
 725				} else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 726					if (conv.hasValidOtrSession()) {
 727						message.setPresence(conv.getOtrSession().getSessionID()
 728								.getUserID());
 729					} else if (!conv.hasValidOtrSession()
 730							&& message.getPresence() != null) {
 731						conv.startOtrSession(getApplicationContext(),
 732								message.getPresence(), false);
 733					}
 734				}
 735			}
 736
 737		}
 738		if (saveInDb) {
 739			databaseBackend.createMessage(message);
 740		}
 741		conv.getMessages().add(message);
 742		if (convChangedListener != null) {
 743			convChangedListener.onConversationListChanged();
 744		}
 745		if ((send) && (packet != null)) {
 746			account.getXmppConnection().sendMessagePacket(packet);
 747		}
 748
 749	}
 750
 751	private void sendUnsendMessages(Conversation conversation) {
 752		for (int i = 0; i < conversation.getMessages().size(); ++i) {
 753			int status = conversation.getMessages().get(i).getStatus();
 754			if (status == Message.STATUS_WAITING) {
 755				resendMessage(conversation.getMessages().get(i));
 756			}
 757		}
 758	}
 759
 760	private void resendMessage(Message message) {
 761		Account account = message.getConversation().getAccount();
 762		if (message.getType() == Message.TYPE_TEXT) {
 763			MessagePacket packet = null;
 764			if (message.getEncryption() == Message.ENCRYPTION_NONE) {
 765				packet = prepareMessagePacket(account, message, null);
 766			} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 767				packet = prepareMessagePacket(account, message, null);
 768				packet.setBody("This is an XEP-0027 encryted message");
 769				if (message.getEncryptedBody() == null) {
 770					markMessage(message, Message.STATUS_SEND_FAILED);
 771					return;
 772				}
 773				packet.addChild("x", "jabber:x:encrypted").setContent(
 774						message.getEncryptedBody());
 775			} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
 776				packet = prepareMessagePacket(account, message, null);
 777				packet.setBody("This is an XEP-0027 encryted message");
 778				packet.addChild("x", "jabber:x:encrypted").setContent(
 779						message.getBody());
 780			} else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 781				Presences presences = message.getConversation().getContact()
 782						.getPresences();
 783				if (!message.getConversation().hasValidOtrSession()) {
 784					if ((message.getPresence() != null)
 785							&& (presences.has(message.getPresence()))) {
 786						message.getConversation().startOtrSession(
 787								getApplicationContext(), message.getPresence(),
 788								true);
 789					} else {
 790						if (presences.size() == 1) {
 791							String presence = presences.asStringArray()[0];
 792							message.getConversation().startOtrSession(
 793									getApplicationContext(), presence, true);
 794						}
 795					}
 796				}
 797			}
 798			if (packet != null) {
 799				account.getXmppConnection().sendMessagePacket(packet);
 800				markMessage(message, Message.STATUS_SEND);
 801			}
 802		} else if (message.getType() == Message.TYPE_IMAGE) {
 803			Presences presences = message.getConversation().getContact()
 804					.getPresences();
 805			if ((message.getPresence() != null)
 806					&& (presences.has(message.getPresence()))) {
 807				markMessage(message, Message.STATUS_OFFERED);
 808				mJingleConnectionManager.createNewConnection(message);
 809			} else {
 810				if (presences.size() == 1) {
 811					String presence = presences.asStringArray()[0];
 812					message.setPresence(presence);
 813					markMessage(message, Message.STATUS_OFFERED);
 814					mJingleConnectionManager.createNewConnection(message);
 815				}
 816			}
 817		}
 818	}
 819
 820	public MessagePacket prepareMessagePacket(Account account, Message message,
 821			Session otrSession) {
 822		MessagePacket packet = new MessagePacket();
 823		if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
 824			packet.setType(MessagePacket.TYPE_CHAT);
 825			packet.setFrom(account.getFullJid());
 826			if (otrSession != null) {
 827				try {
 828					packet.setBody(otrSession.transformSending(message
 829							.getBody()));
 830				} catch (OtrException e) {
 831					Log.d(LOGTAG,
 832							account.getJid()
 833									+ ": could not encrypt message to "
 834									+ message.getCounterpart());
 835				}
 836				packet.addChild("private", "urn:xmpp:carbons:2");
 837				packet.addChild("no-copy", "urn:xmpp:hints");
 838				packet.setTo(otrSession.getSessionID().getAccountID() + "/"
 839						+ otrSession.getSessionID().getUserID());
 840			} else {
 841				packet.setBody(message.getBody());
 842				packet.setTo(message.getCounterpart());
 843			}
 844			packet.addChild("markable", "urn:xmpp:chat-markers:0");
 845		} else if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
 846			packet.setType(MessagePacket.TYPE_GROUPCHAT);
 847			packet.setBody(message.getBody());
 848			packet.setTo(message.getCounterpart().split("/")[0]);
 849			packet.setFrom(account.getJid());
 850		}
 851		packet.setId(message.getUuid());
 852		return packet;
 853	}
 854
 855	public void fetchRosterFromServer(Account account) {
 856		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
 857		if (!"".equals(account.getRosterVersion())) {
 858			Log.d(LOGTAG, account.getJid() + ": fetching roster version "
 859					+ account.getRosterVersion());
 860		} else {
 861			Log.d(LOGTAG, account.getJid() + ": fetching roster");
 862		}
 863		iqPacket.query("jabber:iq:roster").setAttribute("ver",
 864				account.getRosterVersion());
 865		account.getXmppConnection().sendIqPacket(iqPacket,
 866				new OnIqPacketReceived() {
 867
 868					@Override
 869					public void onIqPacketReceived(final Account account,
 870							IqPacket packet) {
 871						Element roster = packet.findChild("query");
 872						if (roster != null) {
 873							account.getRoster().markAllAsNotInRoster();
 874							processRosterItems(account, roster);
 875						}
 876					}
 877				});
 878	}
 879
 880	private void mergePhoneContactsWithRoster() {
 881		PhoneHelper.loadPhoneContacts(getApplicationContext(),
 882				new OnPhoneContactsLoadedListener() {
 883					@Override
 884					public void onPhoneContactsLoaded(List<Bundle> phoneContacts) {
 885						for (Account account : accounts) {
 886							account.getRoster().clearSystemAccounts();
 887						}
 888						for (Bundle phoneContact : phoneContacts) {
 889							for (Account account : accounts) {
 890								String jid = phoneContact.getString("jid");
 891								Contact contact = account.getRoster()
 892										.getContact(jid);
 893								String systemAccount = phoneContact
 894										.getInt("phoneid")
 895										+ "#"
 896										+ phoneContact.getString("lookup");
 897								contact.setSystemAccount(systemAccount);
 898								contact.setPhotoUri(phoneContact
 899										.getString("photouri"));
 900								contact.setSystemName(phoneContact
 901										.getString("displayname"));
 902							}
 903						}
 904					}
 905				});
 906	}
 907
 908	public List<Conversation> getConversations() {
 909		if (this.conversations == null) {
 910			Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
 911			for (Account account : this.accounts) {
 912				accountLookupTable.put(account.getUuid(), account);
 913			}
 914			this.conversations = databaseBackend
 915					.getConversations(Conversation.STATUS_AVAILABLE);
 916			for (Conversation conv : this.conversations) {
 917				Account account = accountLookupTable.get(conv.getAccountUuid());
 918				conv.setAccount(account);
 919				conv.setMessages(databaseBackend.getMessages(conv, 50));
 920			}
 921		}
 922		Collections.sort(this.conversations, new Comparator<Conversation>() {
 923			@Override
 924			public int compare(Conversation lhs, Conversation rhs) {
 925				Message left = lhs.getLatestMessage();
 926				Message right = rhs.getLatestMessage();
 927				if (left.getTimeSent() > right.getTimeSent()) {
 928					return -1;
 929				} else if (left.getTimeSent() < right.getTimeSent()) {
 930					return 1;
 931				} else {
 932					return 0;
 933				}
 934			}
 935		});
 936		return this.conversations;
 937	}
 938
 939	public List<Message> getMoreMessages(Conversation conversation,
 940			long timestamp) {
 941		List<Message> messages = databaseBackend.getMessages(conversation, 50,
 942				timestamp);
 943		for (Message message : messages) {
 944			message.setConversation(conversation);
 945		}
 946		return messages;
 947	}
 948
 949	public List<Account> getAccounts() {
 950		return this.accounts;
 951	}
 952
 953	public Conversation findActiveConversation(Contact contact) {
 954		for (Conversation conversation : this.getConversations()) {
 955			if (conversation.getContact() == contact) {
 956				return conversation;
 957			}
 958		}
 959		return null;
 960	}
 961
 962	public Conversation findOrCreateConversation(Account account, String jid,
 963			boolean muc) {
 964		for (Conversation conv : this.getConversations()) {
 965			if ((conv.getAccount().equals(account))
 966					&& (conv.getContactJid().split("/")[0].equals(jid))) {
 967				return conv;
 968			}
 969		}
 970		Conversation conversation = databaseBackend.findConversation(account,
 971				jid);
 972		if (conversation != null) {
 973			conversation.setStatus(Conversation.STATUS_AVAILABLE);
 974			conversation.setAccount(account);
 975			if (muc) {
 976				conversation.setMode(Conversation.MODE_MULTI);
 977			} else {
 978				conversation.setMode(Conversation.MODE_SINGLE);
 979			}
 980			conversation.setMessages(databaseBackend.getMessages(conversation,
 981					50));
 982			this.databaseBackend.updateConversation(conversation);
 983		} else {
 984			String conversationName;
 985			Contact contact = account.getRoster().getContact(jid);
 986			if (contact != null) {
 987				conversationName = contact.getDisplayName();
 988			} else {
 989				conversationName = jid.split("@")[0];
 990			}
 991			if (muc) {
 992				conversation = new Conversation(conversationName, account, jid,
 993						Conversation.MODE_MULTI);
 994			} else {
 995				conversation = new Conversation(conversationName, account, jid,
 996						Conversation.MODE_SINGLE);
 997			}
 998			this.databaseBackend.createConversation(conversation);
 999		}
1000		this.conversations.add(conversation);
1001		if ((account.getStatus() == Account.STATUS_ONLINE)
1002				&& (conversation.getMode() == Conversation.MODE_MULTI)) {
1003			joinMuc(conversation);
1004		}
1005		if (this.convChangedListener != null) {
1006			this.convChangedListener.onConversationListChanged();
1007		}
1008		return conversation;
1009	}
1010
1011	public void archiveConversation(Conversation conversation) {
1012		if (conversation.getMode() == Conversation.MODE_MULTI) {
1013			leaveMuc(conversation);
1014		} else {
1015			conversation.endOtrIfNeeded();
1016		}
1017		this.databaseBackend.updateConversation(conversation);
1018		this.conversations.remove(conversation);
1019		if (this.convChangedListener != null) {
1020			this.convChangedListener.onConversationListChanged();
1021		}
1022	}
1023
1024	public void clearConversationHistory(Conversation conversation) {
1025		this.databaseBackend.deleteMessagesInConversation(conversation);
1026		this.fileBackend.removeFiles(conversation);
1027		conversation.getMessages().clear();
1028		if (this.convChangedListener != null) {
1029			this.convChangedListener.onConversationListChanged();
1030		}
1031	}
1032
1033	public int getConversationCount() {
1034		return this.databaseBackend.getConversationCount();
1035	}
1036
1037	public void createAccount(Account account) {
1038		databaseBackend.createAccount(account);
1039		this.accounts.add(account);
1040		this.reconnectAccount(account, false);
1041		if (accountChangedListener != null)
1042			accountChangedListener.onAccountListChangedListener();
1043	}
1044
1045	public void updateAccount(Account account) {
1046		this.statusListener.onStatusChanged(account);
1047		databaseBackend.updateAccount(account);
1048		reconnectAccount(account, false);
1049		if (accountChangedListener != null) {
1050			accountChangedListener.onAccountListChangedListener();
1051		}
1052		UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
1053	}
1054
1055	public void deleteAccount(Account account) {
1056		if (account.getXmppConnection() != null) {
1057			this.disconnect(account, true);
1058		}
1059		databaseBackend.deleteAccount(account);
1060		this.accounts.remove(account);
1061		if (accountChangedListener != null) {
1062			accountChangedListener.onAccountListChangedListener();
1063		}
1064		UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
1065	}
1066
1067	public void setOnConversationListChangedListener(
1068			OnConversationListChangedListener listener) {
1069		this.convChangedListener = listener;
1070		this.convChangedListenerCount++;
1071	}
1072
1073	public void removeOnConversationListChangedListener() {
1074		this.convChangedListenerCount--;
1075		if (this.convChangedListenerCount == 0) {
1076			this.convChangedListener = null;
1077		}
1078	}
1079
1080	public void setOnAccountListChangedListener(
1081			OnAccountListChangedListener listener) {
1082		this.accountChangedListener = listener;
1083	}
1084
1085	public void removeOnAccountListChangedListener() {
1086		this.accountChangedListener = null;
1087	}
1088
1089	public void connectMultiModeConversations(Account account) {
1090		List<Conversation> conversations = getConversations();
1091		for (int i = 0; i < conversations.size(); i++) {
1092			Conversation conversation = conversations.get(i);
1093			if ((conversation.getMode() == Conversation.MODE_MULTI)
1094					&& (conversation.getAccount() == account)) {
1095				joinMuc(conversation);
1096			}
1097		}
1098	}
1099
1100	public void joinMuc(Conversation conversation) {
1101		Account account = conversation.getAccount();
1102		String[] mucParts = conversation.getContactJid().split("/");
1103		String muc;
1104		String nick;
1105		if (mucParts.length == 2) {
1106			muc = mucParts[0];
1107			nick = mucParts[1];
1108		} else {
1109			muc = mucParts[0];
1110			nick = account.getUsername();
1111		}
1112		PresencePacket packet = new PresencePacket();
1113		packet.setAttribute("to", muc + "/" + nick);
1114		Element x = new Element("x");
1115		x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
1116		String sig = account.getPgpSignature();
1117		if (sig != null) {
1118			packet.addChild("status").setContent("online");
1119			packet.addChild("x", "jabber:x:signed").setContent(sig);
1120		}
1121		if (conversation.getMessages().size() != 0) {
1122			long lastMsgTime = conversation.getLatestMessage().getTimeSent();
1123			long diff = (System.currentTimeMillis() - lastMsgTime) / 1000 - 1;
1124			x.addChild("history").setAttribute("seconds", diff + "");
1125		}
1126		packet.addChild(x);
1127		account.getXmppConnection().sendPresencePacket(packet);
1128	}
1129
1130	private OnRenameListener renameListener = null;
1131
1132	public void setOnRenameListener(OnRenameListener listener) {
1133		this.renameListener = listener;
1134	}
1135
1136	public void renameInMuc(final Conversation conversation, final String nick) {
1137		final MucOptions options = conversation.getMucOptions();
1138		if (options.online()) {
1139			Account account = conversation.getAccount();
1140			options.setOnRenameListener(new OnRenameListener() {
1141
1142				@Override
1143				public void onRename(boolean success) {
1144					if (renameListener != null) {
1145						renameListener.onRename(success);
1146					}
1147					if (success) {
1148						String jid = conversation.getContactJid().split("/")[0]
1149								+ "/" + nick;
1150						conversation.setContactJid(jid);
1151						databaseBackend.updateConversation(conversation);
1152					}
1153				}
1154			});
1155			options.flagAboutToRename();
1156			PresencePacket packet = new PresencePacket();
1157			packet.setAttribute("to",
1158					conversation.getContactJid().split("/")[0] + "/" + nick);
1159			packet.setAttribute("from", conversation.getAccount().getFullJid());
1160
1161			String sig = account.getPgpSignature();
1162			if (sig != null) {
1163				packet.addChild("status").setContent("online");
1164				packet.addChild("x", "jabber:x:signed").setContent(sig);
1165			}
1166
1167			account.getXmppConnection().sendPresencePacket(packet, null);
1168		} else {
1169			String jid = conversation.getContactJid().split("/")[0] + "/"
1170					+ nick;
1171			conversation.setContactJid(jid);
1172			databaseBackend.updateConversation(conversation);
1173			if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
1174				joinMuc(conversation);
1175			}
1176		}
1177	}
1178
1179	public void leaveMuc(Conversation conversation) {
1180		PresencePacket packet = new PresencePacket();
1181		packet.setAttribute("to", conversation.getContactJid().split("/")[0]
1182				+ "/" + conversation.getMucOptions().getNick());
1183		packet.setAttribute("from", conversation.getAccount().getFullJid());
1184		packet.setAttribute("type", "unavailable");
1185		Log.d(LOGTAG, "send leaving muc " + packet);
1186		conversation.getAccount().getXmppConnection()
1187				.sendPresencePacket(packet);
1188		conversation.getMucOptions().setOffline();
1189	}
1190
1191	public void disconnect(Account account, boolean force) {
1192		if ((account.getStatus() == Account.STATUS_ONLINE)
1193				|| (account.getStatus() == Account.STATUS_DISABLED)) {
1194			if (!force) {
1195				List<Conversation> conversations = getConversations();
1196				for (int i = 0; i < conversations.size(); i++) {
1197					Conversation conversation = conversations.get(i);
1198					if (conversation.getAccount() == account) {
1199						if (conversation.getMode() == Conversation.MODE_MULTI) {
1200							leaveMuc(conversation);
1201						} else {
1202							conversation.endOtrIfNeeded();
1203						}
1204					}
1205				}
1206			}
1207			account.getXmppConnection().disconnect(force);
1208		}
1209	}
1210
1211	@Override
1212	public IBinder onBind(Intent intent) {
1213		return mBinder;
1214	}
1215
1216	public void updateMessage(Message message) {
1217		databaseBackend.updateMessage(message);
1218	}
1219
1220	protected void syncDirtyContacts(Account account) {
1221		for (Contact contact : account.getRoster().getContacts()) {
1222			if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
1223				pushContactToServer(contact);
1224			}
1225			if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
1226				Log.d(LOGTAG, "dirty delete");
1227				deleteContactOnServer(contact);
1228			}
1229		}
1230	}
1231
1232	public void createContact(Contact contact) {
1233		SharedPreferences sharedPref = getPreferences();
1234		boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1235		if (autoGrant) {
1236			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
1237			contact.setOption(Contact.Options.ASKING);
1238		}
1239		pushContactToServer(contact);
1240	}
1241
1242	public void onOtrSessionEstablished(Conversation conversation) {
1243		Account account = conversation.getAccount();
1244		List<Message> messages = conversation.getMessages();
1245		Session otrSession = conversation.getOtrSession();
1246		Log.d(LOGTAG, account.getJid() + " otr session established with "
1247				+ conversation.getContactJid() + "/"
1248				+ otrSession.getSessionID().getUserID());
1249		for (int i = 0; i < messages.size(); ++i) {
1250			Message msg = messages.get(i);
1251			if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING)
1252					&& (msg.getEncryption() == Message.ENCRYPTION_OTR)) {
1253				msg.setPresence(otrSession.getSessionID().getUserID());
1254				if (msg.getType() == Message.TYPE_TEXT) {
1255					MessagePacket outPacket = prepareMessagePacket(account,
1256							msg, otrSession);
1257					msg.setStatus(Message.STATUS_SEND);
1258					databaseBackend.updateMessage(msg);
1259					account.getXmppConnection().sendMessagePacket(outPacket);
1260				} else if (msg.getType() == Message.TYPE_IMAGE) {
1261					mJingleConnectionManager.createNewConnection(msg);
1262				}
1263			}
1264		}
1265		updateUi(conversation, false);
1266	}
1267
1268	public boolean renewSymmetricKey(Conversation conversation) {
1269		Account account = conversation.getAccount();
1270		byte[] symmetricKey = new byte[32];
1271		this.mRandom.nextBytes(symmetricKey);
1272		Session otrSession = conversation.getOtrSession();
1273		if (otrSession != null) {
1274			MessagePacket packet = new MessagePacket();
1275			packet.setType(MessagePacket.TYPE_CHAT);
1276			packet.setFrom(account.getFullJid());
1277			packet.addChild("private", "urn:xmpp:carbons:2");
1278			packet.addChild("no-copy", "urn:xmpp:hints");
1279			packet.setTo(otrSession.getSessionID().getAccountID() + "/"
1280					+ otrSession.getSessionID().getUserID());
1281			try {
1282				packet.setBody(otrSession
1283						.transformSending(CryptoHelper.FILETRANSFER
1284								+ CryptoHelper.bytesToHex(symmetricKey)));
1285				account.getXmppConnection().sendMessagePacket(packet);
1286				conversation.setSymmetricKey(symmetricKey);
1287				return true;
1288			} catch (OtrException e) {
1289				return false;
1290			}
1291		}
1292		return false;
1293	}
1294
1295	public void pushContactToServer(Contact contact) {
1296		contact.resetOption(Contact.Options.DIRTY_DELETE);
1297		Account account = contact.getAccount();
1298		if (account.getStatus() == Account.STATUS_ONLINE) {
1299			IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1300			iq.query("jabber:iq:roster").addChild(contact.asElement());
1301			account.getXmppConnection().sendIqPacket(iq, null);
1302			if (contact.getOption(Contact.Options.ASKING)) {
1303				requestPresenceUpdatesFrom(contact);
1304			}
1305			if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1306				Log.d("xmppService", "contact had pending subscription");
1307				sendPresenceUpdatesTo(contact);
1308			}
1309			contact.resetOption(Contact.Options.DIRTY_PUSH);
1310		} else {
1311			contact.setOption(Contact.Options.DIRTY_PUSH);
1312		}
1313	}
1314
1315	public void deleteContactOnServer(Contact contact) {
1316		contact.resetOption(Contact.Options.DIRTY_PUSH);
1317		Account account = contact.getAccount();
1318		if (account.getStatus() == Account.STATUS_ONLINE) {
1319			IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1320			Element item = iq.query("jabber:iq:roster").addChild("item");
1321			item.setAttribute("jid", contact.getJid());
1322			item.setAttribute("subscription", "remove");
1323			account.getXmppConnection().sendIqPacket(iq, null);
1324			contact.resetOption(Contact.Options.DIRTY_DELETE);
1325		} else {
1326			contact.setOption(Contact.Options.DIRTY_DELETE);
1327		}
1328	}
1329
1330	public void requestPresenceUpdatesFrom(Contact contact) {
1331		PresencePacket packet = new PresencePacket();
1332		packet.setAttribute("type", "subscribe");
1333		packet.setAttribute("to", contact.getJid());
1334		packet.setAttribute("from", contact.getAccount().getJid());
1335		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1336	}
1337
1338	public void stopPresenceUpdatesFrom(Contact contact) {
1339		PresencePacket packet = new PresencePacket();
1340		packet.setAttribute("type", "unsubscribe");
1341		packet.setAttribute("to", contact.getJid());
1342		packet.setAttribute("from", contact.getAccount().getJid());
1343		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1344	}
1345
1346	public void stopPresenceUpdatesTo(Contact contact) {
1347		PresencePacket packet = new PresencePacket();
1348		packet.setAttribute("type", "unsubscribed");
1349		packet.setAttribute("to", contact.getJid());
1350		packet.setAttribute("from", contact.getAccount().getJid());
1351		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1352	}
1353
1354	public void sendPresenceUpdatesTo(Contact contact) {
1355		PresencePacket packet = new PresencePacket();
1356		packet.setAttribute("type", "subscribed");
1357		packet.setAttribute("to", contact.getJid());
1358		packet.setAttribute("from", contact.getAccount().getJid());
1359		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1360	}
1361
1362	public void sendPresence(Account account) {
1363		PresencePacket packet = new PresencePacket();
1364		packet.setAttribute("from", account.getFullJid());
1365		String sig = account.getPgpSignature();
1366		if (sig != null) {
1367			packet.addChild("status").setContent("online");
1368			packet.addChild("x", "jabber:x:signed").setContent(sig);
1369		}
1370		account.getXmppConnection().sendPresencePacket(packet);
1371	}
1372
1373	public void updateConversation(Conversation conversation) {
1374		this.databaseBackend.updateConversation(conversation);
1375	}
1376
1377	public void removeOnTLSExceptionReceivedListener() {
1378		this.tlsException = null;
1379	}
1380
1381	public void reconnectAccount(final Account account, final boolean force) {
1382		new Thread(new Runnable() {
1383
1384			@Override
1385			public void run() {
1386				if (account.getXmppConnection() != null) {
1387					disconnect(account, force);
1388				}
1389				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1390					if (account.getXmppConnection() == null) {
1391						account.setXmppConnection(createConnection(account));
1392					}
1393					Thread thread = new Thread(account.getXmppConnection());
1394					thread.start();
1395					scheduleWakeupCall((int) (CONNECT_TIMEOUT * 1.2), false);
1396				}
1397			}
1398		}).start();
1399	}
1400
1401	public void sendConversationSubject(Conversation conversation,
1402			String subject) {
1403		MessagePacket packet = new MessagePacket();
1404		packet.setType(MessagePacket.TYPE_GROUPCHAT);
1405		packet.setTo(conversation.getContactJid().split("/")[0]);
1406		Element subjectChild = new Element("subject");
1407		subjectChild.setContent(subject);
1408		packet.addChild(subjectChild);
1409		packet.setFrom(conversation.getAccount().getJid());
1410		Account account = conversation.getAccount();
1411		if (account.getStatus() == Account.STATUS_ONLINE) {
1412			account.getXmppConnection().sendMessagePacket(packet);
1413		}
1414	}
1415
1416	public void inviteToConference(Conversation conversation,
1417			List<Contact> contacts) {
1418		for (Contact contact : contacts) {
1419			MessagePacket packet = new MessagePacket();
1420			packet.setTo(conversation.getContactJid().split("/")[0]);
1421			packet.setFrom(conversation.getAccount().getFullJid());
1422			Element x = new Element("x");
1423			x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
1424			Element invite = new Element("invite");
1425			invite.setAttribute("to", contact.getJid());
1426			x.addChild(invite);
1427			packet.addChild(x);
1428			Log.d(LOGTAG, packet.toString());
1429			conversation.getAccount().getXmppConnection()
1430					.sendMessagePacket(packet);
1431		}
1432
1433	}
1434
1435	public boolean markMessage(Account account, String recipient, String uuid,
1436			int status) {
1437		for (Conversation conversation : getConversations()) {
1438			if (conversation.getContactJid().equals(recipient)
1439					&& conversation.getAccount().equals(account)) {
1440				return markMessage(conversation, uuid, status);
1441			}
1442		}
1443		return false;
1444	}
1445
1446	public boolean markMessage(Conversation conversation, String uuid,
1447			int status) {
1448		for (Message message : conversation.getMessages()) {
1449			if (message.getUuid().equals(uuid)) {
1450				markMessage(message, status);
1451				return true;
1452			}
1453		}
1454		return false;
1455	}
1456
1457	public void markMessage(Message message, int status) {
1458		message.setStatus(status);
1459		databaseBackend.updateMessage(message);
1460		if (convChangedListener != null) {
1461			convChangedListener.onConversationListChanged();
1462		}
1463	}
1464
1465	public SharedPreferences getPreferences() {
1466		return PreferenceManager
1467				.getDefaultSharedPreferences(getApplicationContext());
1468	}
1469
1470	public boolean confirmMessages() {
1471		return getPreferences().getBoolean("confirm_messages", true);
1472	}
1473
1474	public void updateUi(Conversation conversation, boolean notify) {
1475		if (convChangedListener != null) {
1476			convChangedListener.onConversationListChanged();
1477		} else {
1478			UIHelper.updateNotification(getApplicationContext(),
1479					getConversations(), conversation, notify);
1480		}
1481	}
1482
1483	public Account findAccountByJid(String accountJid) {
1484		for (Account account : this.accounts) {
1485			if (account.getJid().equals(accountJid)) {
1486				return account;
1487			}
1488		}
1489		return null;
1490	}
1491
1492	public void markRead(Conversation conversation) {
1493		conversation.markRead(this);
1494	}
1495
1496	public void sendConfirmMessage(Account account, String to, String id) {
1497		MessagePacket receivedPacket = new MessagePacket();
1498		receivedPacket.setType(MessagePacket.TYPE_NORMAL);
1499		receivedPacket.setTo(to);
1500		receivedPacket.setFrom(account.getFullJid());
1501		Element received = receivedPacket.addChild("displayed",
1502				"urn:xmpp:chat-markers:0");
1503		received.setAttribute("id", id);
1504		account.getXmppConnection().sendMessagePacket(receivedPacket);
1505	}
1506
1507	public SecureRandom getRNG() {
1508		return this.mRandom;
1509	}
1510
1511	public PowerManager getPowerManager() {
1512		return this.pm;
1513	}
1514}