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 (Bundle phoneContact : phoneContacts) {
 898							for (Account account : accounts) {
 899								String jid = phoneContact.getString("jid");
 900								Contact contact = account.getRoster()
 901										.getContact(jid);
 902								String systemAccount = phoneContact
 903										.getInt("phoneid")
 904										+ "#"
 905										+ phoneContact.getString("lookup");
 906								contact.setSystemAccount(systemAccount);
 907								contact.setPhotoUri(phoneContact
 908										.getString("photouri"));
 909								contact.setSystemName(phoneContact
 910										.getString("displayname"));
 911							}
 912						}
 913					}
 914				});
 915	}
 916
 917	public List<Conversation> getConversations() {
 918		if (this.conversations == null) {
 919			Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
 920			for (Account account : this.accounts) {
 921				accountLookupTable.put(account.getUuid(), account);
 922			}
 923			this.conversations = databaseBackend
 924					.getConversations(Conversation.STATUS_AVAILABLE);
 925			for (Conversation conv : this.conversations) {
 926				Account account = accountLookupTable.get(conv.getAccountUuid());
 927				conv.setAccount(account);
 928				conv.setMessages(databaseBackend.getMessages(conv, 50));
 929			}
 930		}
 931		Collections.sort(this.conversations, new Comparator<Conversation>() {
 932			@Override
 933			public int compare(Conversation lhs, Conversation rhs) {
 934				return (int) (rhs.getLatestMessage().getTimeSent() - lhs
 935						.getLatestMessage().getTimeSent());
 936			}
 937		});
 938		return this.conversations;
 939	}
 940
 941	public List<Account> getAccounts() {
 942		return this.accounts;
 943	}
 944
 945	public Conversation findOrCreateConversation(Account account, String jid,
 946			boolean muc) {
 947		for (Conversation conv : this.getConversations()) {
 948			if ((conv.getAccount().equals(account))
 949					&& (conv.getContactJid().split("/")[0].equals(jid))) {
 950				return conv;
 951			}
 952		}
 953		Conversation conversation = databaseBackend.findConversation(account,
 954				jid);
 955		if (conversation != null) {
 956			conversation.setStatus(Conversation.STATUS_AVAILABLE);
 957			conversation.setAccount(account);
 958			if (muc) {
 959				conversation.setMode(Conversation.MODE_MULTI);
 960			} else {
 961				conversation.setMode(Conversation.MODE_SINGLE);
 962			}
 963			conversation.setMessages(databaseBackend.getMessages(conversation,
 964					50));
 965			this.databaseBackend.updateConversation(conversation);
 966		} else {
 967			String conversationName;
 968			Contact contact = account.getRoster().getContact(jid);
 969			if (contact != null) {
 970				conversationName = contact.getDisplayName();
 971			} else {
 972				conversationName = jid.split("@")[0];
 973			}
 974			if (muc) {
 975				conversation = new Conversation(conversationName, account, jid,
 976						Conversation.MODE_MULTI);
 977			} else {
 978				conversation = new Conversation(conversationName, account, jid,
 979						Conversation.MODE_SINGLE);
 980			}
 981			this.databaseBackend.createConversation(conversation);
 982		}
 983		this.conversations.add(conversation);
 984		if ((account.getStatus() == Account.STATUS_ONLINE)
 985				&& (conversation.getMode() == Conversation.MODE_MULTI)) {
 986			joinMuc(conversation);
 987		}
 988		if (this.convChangedListener != null) {
 989			this.convChangedListener.onConversationListChanged();
 990		}
 991		return conversation;
 992	}
 993
 994	public void archiveConversation(Conversation conversation) {
 995		if (conversation.getMode() == Conversation.MODE_MULTI) {
 996			leaveMuc(conversation);
 997		} else {
 998			conversation.endOtrIfNeeded();
 999		}
1000		this.databaseBackend.updateConversation(conversation);
1001		this.conversations.remove(conversation);
1002		if (this.convChangedListener != null) {
1003			this.convChangedListener.onConversationListChanged();
1004		}
1005	}
1006
1007	public void clearConversationHistory(Conversation conversation) {
1008		this.databaseBackend.deleteMessagesInConversation(conversation);
1009		this.fileBackend.removeFiles(conversation);
1010		conversation.getMessages().clear();
1011		if (this.convChangedListener != null) {
1012			this.convChangedListener.onConversationListChanged();
1013		}
1014	}
1015
1016	public int getConversationCount() {
1017		return this.databaseBackend.getConversationCount();
1018	}
1019
1020	public void createAccount(Account account) {
1021		databaseBackend.createAccount(account);
1022		this.accounts.add(account);
1023		this.reconnectAccount(account, false);
1024		if (accountChangedListener != null)
1025			accountChangedListener.onAccountListChangedListener();
1026	}
1027
1028	public void updateAccount(Account account) {
1029		this.statusListener.onStatusChanged(account);
1030		databaseBackend.updateAccount(account);
1031		reconnectAccount(account, false);
1032		if (accountChangedListener != null) {
1033			accountChangedListener.onAccountListChangedListener();
1034		}
1035		UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
1036	}
1037
1038	public void deleteAccount(Account account) {
1039		if (account.getXmppConnection() != null) {
1040			this.disconnect(account, true);
1041		}
1042		databaseBackend.deleteAccount(account);
1043		this.accounts.remove(account);
1044		if (accountChangedListener != null) {
1045			accountChangedListener.onAccountListChangedListener();
1046		}
1047		UIHelper.showErrorNotification(getApplicationContext(), getAccounts());
1048	}
1049
1050	public void setOnConversationListChangedListener(
1051			OnConversationListChangedListener listener) {
1052		this.convChangedListener = listener;
1053		this.convChangedListenerCount++;
1054	}
1055
1056	public void removeOnConversationListChangedListener() {
1057		this.convChangedListenerCount--;
1058		if (this.convChangedListenerCount == 0) {
1059			this.convChangedListener = null;
1060		}
1061	}
1062
1063	public void setOnAccountListChangedListener(
1064			OnAccountListChangedListener listener) {
1065		this.accountChangedListener = listener;
1066	}
1067
1068	public void removeOnAccountListChangedListener() {
1069		this.accountChangedListener = null;
1070	}
1071
1072	public void connectMultiModeConversations(Account account) {
1073		List<Conversation> conversations = getConversations();
1074		for (int i = 0; i < conversations.size(); i++) {
1075			Conversation conversation = conversations.get(i);
1076			if ((conversation.getMode() == Conversation.MODE_MULTI)
1077					&& (conversation.getAccount() == account)) {
1078				joinMuc(conversation);
1079			}
1080		}
1081	}
1082
1083	public void joinMuc(Conversation conversation) {
1084		String[] mucParts = conversation.getContactJid().split("/");
1085		String muc;
1086		String nick;
1087		if (mucParts.length == 2) {
1088			muc = mucParts[0];
1089			nick = mucParts[1];
1090		} else {
1091			muc = mucParts[0];
1092			nick = conversation.getAccount().getUsername();
1093		}
1094		PresencePacket packet = new PresencePacket();
1095		packet.setAttribute("to", muc + "/" + nick);
1096		Element x = new Element("x");
1097		x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
1098		if (conversation.getMessages().size() != 0) {
1099			long lastMsgTime = conversation.getLatestMessage().getTimeSent();
1100			long diff = (System.currentTimeMillis() - lastMsgTime) / 1000 - 1;
1101			x.addChild("history").setAttribute("seconds", diff + "");
1102		}
1103		packet.addChild(x);
1104		conversation.getAccount().getXmppConnection()
1105				.sendPresencePacket(packet);
1106	}
1107
1108	private OnRenameListener renameListener = null;
1109
1110	public void setOnRenameListener(OnRenameListener listener) {
1111		this.renameListener = listener;
1112	}
1113
1114	public void renameInMuc(final Conversation conversation, final String nick) {
1115		final MucOptions options = conversation.getMucOptions();
1116		if (options.online()) {
1117			options.setOnRenameListener(new OnRenameListener() {
1118
1119				@Override
1120				public void onRename(boolean success) {
1121					if (renameListener != null) {
1122						renameListener.onRename(success);
1123					}
1124					if (success) {
1125						String jid = conversation.getContactJid().split("/")[0]
1126								+ "/" + nick;
1127						conversation.setContactJid(jid);
1128						databaseBackend.updateConversation(conversation);
1129					}
1130				}
1131			});
1132			options.flagAboutToRename();
1133			PresencePacket packet = new PresencePacket();
1134			packet.setAttribute("to",
1135					conversation.getContactJid().split("/")[0] + "/" + nick);
1136			packet.setAttribute("from", conversation.getAccount().getFullJid());
1137
1138			conversation.getAccount().getXmppConnection()
1139					.sendPresencePacket(packet, null);
1140		} else {
1141			String jid = conversation.getContactJid().split("/")[0] + "/"
1142					+ nick;
1143			conversation.setContactJid(jid);
1144			databaseBackend.updateConversation(conversation);
1145			if (conversation.getAccount().getStatus() == Account.STATUS_ONLINE) {
1146				joinMuc(conversation);
1147			}
1148		}
1149	}
1150
1151	public void leaveMuc(Conversation conversation) {
1152		PresencePacket packet = new PresencePacket();
1153		packet.setAttribute("to", conversation.getContactJid().split("/")[0]
1154				+ "/" + conversation.getMucOptions().getNick());
1155		packet.setAttribute("from", conversation.getAccount().getFullJid());
1156		packet.setAttribute("type", "unavailable");
1157		Log.d(LOGTAG, "send leaving muc " + packet);
1158		conversation.getAccount().getXmppConnection()
1159				.sendPresencePacket(packet);
1160		conversation.getMucOptions().setOffline();
1161	}
1162
1163	public void disconnect(Account account, boolean force) {
1164		if ((account.getStatus() == Account.STATUS_ONLINE)
1165				|| (account.getStatus() == Account.STATUS_DISABLED)) {
1166			if (!force) {
1167				List<Conversation> conversations = getConversations();
1168				for (int i = 0; i < conversations.size(); i++) {
1169					Conversation conversation = conversations.get(i);
1170					if (conversation.getAccount() == account) {
1171						if (conversation.getMode() == Conversation.MODE_MULTI) {
1172							leaveMuc(conversation);
1173						} else {
1174							conversation.endOtrIfNeeded();
1175						}
1176					}
1177				}
1178			}
1179			account.getXmppConnection().disconnect(force);
1180		}
1181	}
1182
1183	@Override
1184	public IBinder onBind(Intent intent) {
1185		return mBinder;
1186	}
1187
1188	public void updateMessage(Message message) {
1189		databaseBackend.updateMessage(message);
1190	}
1191
1192	public void createContact(Contact contact) {
1193		SharedPreferences sharedPref = getPreferences();
1194		boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1195		if (autoGrant) {
1196			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
1197			contact.setOption(Contact.Options.ASKING);
1198		}
1199		pushContactToServer(contact);
1200	}
1201
1202	public void pushContactToServer(Contact contact) {
1203		Account account = contact.getAccount();
1204		if (account.getStatus() == Account.STATUS_ONLINE) {
1205			IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1206			iq.query("jabber:iq:roster").addChild(contact.asElement());
1207			account.getXmppConnection().sendIqPacket(iq, null);
1208			contact.resetOption(Contact.Options.DIRTY_PUSH);
1209			if (contact.getOption(Contact.Options.ASKING)) {
1210				requestPresenceUpdatesFrom(contact);
1211			}
1212			if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
1213				Log.d("xmppService", "contact had pending subscription");
1214				sendPresenceUpdatesTo(contact);
1215			}
1216			contact.resetOption(Contact.Options.DIRTY_PUSH);
1217		} else {
1218			contact.setOption(Contact.Options.DIRTY_PUSH);
1219		}
1220	}
1221
1222	public void deleteContactOnServer(Contact contact) {
1223		Account account = contact.getAccount();
1224		if (account.getStatus() == Account.STATUS_ONLINE) {
1225			IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
1226			Element item = iq.query("jabber:iq:roster").addChild("item");
1227			item.setAttribute("jid", contact.getJid());
1228			item.setAttribute("subscription", "remove");
1229			account.getXmppConnection().sendIqPacket(iq, null);
1230			contact.resetOption(Contact.Options.DIRTY_DELETE);
1231		} else {
1232			contact.setOption(Contact.Options.DIRTY_DELETE);
1233		}
1234	}
1235
1236	public void requestPresenceUpdatesFrom(Contact contact) {
1237		PresencePacket packet = new PresencePacket();
1238		packet.setAttribute("type", "subscribe");
1239		packet.setAttribute("to", contact.getJid());
1240		packet.setAttribute("from", contact.getAccount().getJid());
1241		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1242	}
1243
1244	public void stopPresenceUpdatesFrom(Contact contact) {
1245		PresencePacket packet = new PresencePacket();
1246		packet.setAttribute("type", "unsubscribe");
1247		packet.setAttribute("to", contact.getJid());
1248		packet.setAttribute("from", contact.getAccount().getJid());
1249		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1250	}
1251
1252	public void stopPresenceUpdatesTo(Contact contact) {
1253		PresencePacket packet = new PresencePacket();
1254		packet.setAttribute("type", "unsubscribed");
1255		packet.setAttribute("to", contact.getJid());
1256		packet.setAttribute("from", contact.getAccount().getJid());
1257		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1258	}
1259
1260	public void sendPresenceUpdatesTo(Contact contact) {
1261		PresencePacket packet = new PresencePacket();
1262		packet.setAttribute("type", "subscribed");
1263		packet.setAttribute("to", contact.getJid());
1264		packet.setAttribute("from", contact.getAccount().getJid());
1265		contact.getAccount().getXmppConnection().sendPresencePacket(packet);
1266		contact.resetOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST);
1267	}
1268
1269	public void sendPresence(Account account) {
1270		PresencePacket packet = new PresencePacket();
1271		packet.setAttribute("from", account.getFullJid());
1272		String sig = account.getPgpSignature();
1273		if (sig != null) {
1274			packet.addChild("status").setContent("online");
1275			packet.addChild("x", "jabber:x:signed").setContent(sig);
1276		}
1277		account.getXmppConnection().sendPresencePacket(packet);
1278	}
1279
1280	public void updateConversation(Conversation conversation) {
1281		this.databaseBackend.updateConversation(conversation);
1282	}
1283
1284	public void removeOnTLSExceptionReceivedListener() {
1285		this.tlsException = null;
1286	}
1287
1288	public void reconnectAccount(final Account account, final boolean force) {
1289		new Thread(new Runnable() {
1290
1291			@Override
1292			public void run() {
1293				if (account.getXmppConnection() != null) {
1294					disconnect(account, force);
1295				}
1296				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
1297					if (account.getXmppConnection() == null) {
1298						account.setXmppConnection(createConnection(account));
1299					}
1300					Thread thread = new Thread(account.getXmppConnection());
1301					thread.start();
1302					scheduleWakeupCall((int) (CONNECT_TIMEOUT * 1.2), false);
1303				}
1304			}
1305		}).start();
1306	}
1307
1308	public void sendConversationSubject(Conversation conversation,
1309			String subject) {
1310		MessagePacket packet = new MessagePacket();
1311		packet.setType(MessagePacket.TYPE_GROUPCHAT);
1312		packet.setTo(conversation.getContactJid().split("/")[0]);
1313		Element subjectChild = new Element("subject");
1314		subjectChild.setContent(subject);
1315		packet.addChild(subjectChild);
1316		packet.setFrom(conversation.getAccount().getJid());
1317		Account account = conversation.getAccount();
1318		if (account.getStatus() == Account.STATUS_ONLINE) {
1319			account.getXmppConnection().sendMessagePacket(packet);
1320		}
1321	}
1322
1323	public void inviteToConference(Conversation conversation,
1324			List<Contact> contacts) {
1325		for (Contact contact : contacts) {
1326			MessagePacket packet = new MessagePacket();
1327			packet.setTo(conversation.getContactJid().split("/")[0]);
1328			packet.setFrom(conversation.getAccount().getFullJid());
1329			Element x = new Element("x");
1330			x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user");
1331			Element invite = new Element("invite");
1332			invite.setAttribute("to", contact.getJid());
1333			x.addChild(invite);
1334			packet.addChild(x);
1335			Log.d(LOGTAG, packet.toString());
1336			conversation.getAccount().getXmppConnection()
1337					.sendMessagePacket(packet);
1338		}
1339
1340	}
1341
1342	public boolean markMessage(Account account, String recipient, String uuid,
1343			int status) {
1344		for (Conversation conversation : getConversations()) {
1345			if (conversation.getContactJid().equals(recipient)
1346					&& conversation.getAccount().equals(account)) {
1347				return markMessage(conversation, uuid, status);
1348			}
1349		}
1350		return false;
1351	}
1352
1353	public boolean markMessage(Conversation conversation, String uuid,
1354			int status) {
1355		for (Message message : conversation.getMessages()) {
1356			if (message.getUuid().equals(uuid)) {
1357				markMessage(message, status);
1358				return true;
1359			}
1360		}
1361		return false;
1362	}
1363
1364	public void markMessage(Message message, int status) {
1365		message.setStatus(status);
1366		databaseBackend.updateMessage(message);
1367		if (convChangedListener != null) {
1368			convChangedListener.onConversationListChanged();
1369		}
1370	}
1371
1372	public SharedPreferences getPreferences() {
1373		return PreferenceManager
1374				.getDefaultSharedPreferences(getApplicationContext());
1375	}
1376
1377	public void updateUi(Conversation conversation, boolean notify) {
1378		if (convChangedListener != null) {
1379			convChangedListener.onConversationListChanged();
1380		} else {
1381			UIHelper.updateNotification(getApplicationContext(),
1382					getConversations(), conversation, notify);
1383		}
1384	}
1385
1386	public Account findAccountByJid(String accountJid) {
1387		for (Account account : this.accounts) {
1388			if (account.getJid().equals(accountJid)) {
1389				return account;
1390			}
1391		}
1392		return null;
1393	}
1394}