XmppConnectionService.java

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