XmppConnectionService.java

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