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