XmppConnectionService.java

   1package eu.siacs.conversations.services;
   2
   3import android.annotation.SuppressLint;
   4import android.app.AlarmManager;
   5import android.app.PendingIntent;
   6import android.app.Service;
   7import android.content.Context;
   8import android.content.Intent;
   9import android.content.SharedPreferences;
  10import android.database.ContentObserver;
  11import android.graphics.Bitmap;
  12import android.net.ConnectivityManager;
  13import android.net.NetworkInfo;
  14import android.net.Uri;
  15import android.os.Binder;
  16import android.os.Bundle;
  17import android.os.FileObserver;
  18import android.os.IBinder;
  19import android.os.Looper;
  20import android.os.PowerManager;
  21import android.os.PowerManager.WakeLock;
  22import android.os.SystemClock;
  23import android.preference.PreferenceManager;
  24import android.provider.ContactsContract;
  25import android.util.Log;
  26import android.util.LruCache;
  27
  28import net.java.otr4j.OtrException;
  29import net.java.otr4j.session.Session;
  30import net.java.otr4j.session.SessionID;
  31import net.java.otr4j.session.SessionStatus;
  32
  33import org.openintents.openpgp.util.OpenPgpApi;
  34import org.openintents.openpgp.util.OpenPgpServiceConnection;
  35
  36import java.math.BigInteger;
  37import java.security.SecureRandom;
  38import java.util.ArrayList;
  39import java.util.Arrays;
  40import java.util.Collection;
  41import java.util.Collections;
  42import java.util.Comparator;
  43import java.util.Hashtable;
  44import java.util.Iterator;
  45import java.util.List;
  46import java.util.Locale;
  47import java.util.Map;
  48import java.util.concurrent.CopyOnWriteArrayList;
  49
  50import de.duenndns.ssl.MemorizingTrustManager;
  51import eu.siacs.conversations.Config;
  52import eu.siacs.conversations.R;
  53import eu.siacs.conversations.crypto.PgpEngine;
  54import eu.siacs.conversations.entities.Account;
  55import eu.siacs.conversations.entities.Blockable;
  56import eu.siacs.conversations.entities.Bookmark;
  57import eu.siacs.conversations.entities.Contact;
  58import eu.siacs.conversations.entities.Conversation;
  59import eu.siacs.conversations.entities.Downloadable;
  60import eu.siacs.conversations.entities.DownloadablePlaceholder;
  61import eu.siacs.conversations.entities.Message;
  62import eu.siacs.conversations.entities.MucOptions;
  63import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
  64import eu.siacs.conversations.entities.Presences;
  65import eu.siacs.conversations.generator.IqGenerator;
  66import eu.siacs.conversations.generator.MessageGenerator;
  67import eu.siacs.conversations.generator.PresenceGenerator;
  68import eu.siacs.conversations.http.HttpConnectionManager;
  69import eu.siacs.conversations.parser.IqParser;
  70import eu.siacs.conversations.parser.MessageParser;
  71import eu.siacs.conversations.parser.PresenceParser;
  72import eu.siacs.conversations.persistance.DatabaseBackend;
  73import eu.siacs.conversations.persistance.FileBackend;
  74import eu.siacs.conversations.ui.UiCallback;
  75import eu.siacs.conversations.utils.CryptoHelper;
  76import eu.siacs.conversations.utils.ExceptionHelper;
  77import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
  78import eu.siacs.conversations.utils.PRNGFixes;
  79import eu.siacs.conversations.utils.PhoneHelper;
  80import eu.siacs.conversations.utils.Xmlns;
  81import eu.siacs.conversations.xml.Element;
  82import eu.siacs.conversations.xmpp.OnBindListener;
  83import eu.siacs.conversations.xmpp.OnContactStatusChanged;
  84import eu.siacs.conversations.xmpp.OnIqPacketReceived;
  85import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
  86import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
  87import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
  88import eu.siacs.conversations.xmpp.OnStatusChanged;
  89import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
  90import eu.siacs.conversations.xmpp.XmppConnection;
  91import eu.siacs.conversations.xmpp.chatstate.ChatState;
  92import eu.siacs.conversations.xmpp.forms.Data;
  93import eu.siacs.conversations.xmpp.forms.Field;
  94import eu.siacs.conversations.xmpp.jid.InvalidJidException;
  95import eu.siacs.conversations.xmpp.jid.Jid;
  96import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
  97import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived;
  98import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
  99import eu.siacs.conversations.xmpp.pep.Avatar;
 100import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 101import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 102import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
 103
 104public class XmppConnectionService extends Service implements OnPhoneContactsLoadedListener {
 105
 106	public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification";
 107	public static final String ACTION_DISABLE_FOREGROUND = "disable_foreground";
 108	private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts";
 109	public static final String ACTION_TRY_AGAIN = "try_again";
 110	public static final String ACTION_DISABLE_ACCOUNT = "disable_account";
 111	private ContentObserver contactObserver = new ContentObserver(null) {
 112		@Override
 113		public void onChange(boolean selfChange) {
 114			super.onChange(selfChange);
 115			Intent intent = new Intent(getApplicationContext(),
 116					XmppConnectionService.class);
 117			intent.setAction(ACTION_MERGE_PHONE_CONTACTS);
 118			startService(intent);
 119		}
 120	};
 121	private final IBinder mBinder = new XmppConnectionBinder();
 122	private final List<Conversation> conversations = new CopyOnWriteArrayList<>();
 123	private final FileObserver fileObserver = new FileObserver(
 124			FileBackend.getConversationsImageDirectory()) {
 125
 126		@Override
 127		public void onEvent(int event, String path) {
 128			if (event == FileObserver.DELETE) {
 129				markFileDeleted(path.split("\\.")[0]);
 130			}
 131		}
 132	};
 133	private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() {
 134
 135		@Override
 136		public void onJinglePacketReceived(Account account, JinglePacket packet) {
 137			mJingleConnectionManager.deliverPacket(account, packet);
 138		}
 139	};
 140	private final OnBindListener mOnBindListener = new OnBindListener() {
 141
 142		@Override
 143		public void onBind(final Account account) {
 144			account.getRoster().clearPresences();
 145			account.pendingConferenceJoins.clear();
 146			account.pendingConferenceLeaves.clear();
 147			fetchRosterFromServer(account);
 148			fetchBookmarks(account);
 149			sendPresence(account);
 150			connectMultiModeConversations(account);
 151			updateConversationUi();
 152		}
 153	};
 154	private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() {
 155
 156		@Override
 157		public void onMessageAcknowledged(Account account, String uuid) {
 158			for (final Conversation conversation : getConversations()) {
 159				if (conversation.getAccount() == account) {
 160					Message message = conversation.findUnsentMessageWithUuid(uuid);
 161					if (message != null) {
 162						markMessage(message, Message.STATUS_SEND);
 163						if (conversation.setLastMessageTransmitted(System.currentTimeMillis())) {
 164							databaseBackend.updateConversation(conversation);
 165						}
 166					}
 167				}
 168			}
 169		}
 170	};
 171	private final IqGenerator mIqGenerator = new IqGenerator(this);
 172	public DatabaseBackend databaseBackend;
 173	public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
 174
 175		@Override
 176		public void onContactStatusChanged(Contact contact, boolean online) {
 177			Conversation conversation = find(getConversations(), contact);
 178			if (conversation != null) {
 179				if (online) {
 180					conversation.endOtrIfNeeded();
 181					if (contact.getPresences().size() == 1) {
 182						sendUnsentMessages(conversation);
 183					}
 184				} else {
 185					if (contact.getPresences().size() >= 1) {
 186						if (conversation.hasValidOtrSession()) {
 187							String otrResource = conversation.getOtrSession().getSessionID().getUserID();
 188							if (!(Arrays.asList(contact.getPresences().asStringArray()).contains(otrResource))) {
 189								conversation.endOtrIfNeeded();
 190							}
 191						}
 192					} else {
 193						conversation.endOtrIfNeeded();
 194					}
 195				}
 196			}
 197		}
 198	};
 199	private FileBackend fileBackend = new FileBackend(this);
 200	private MemorizingTrustManager mMemorizingTrustManager;
 201	private NotificationService mNotificationService = new NotificationService(
 202			this);
 203	private OnMessagePacketReceived mMessageParser = new MessageParser(this);
 204	private OnPresencePacketReceived mPresenceParser = new PresenceParser(this);
 205	private IqParser mIqParser = new IqParser(this);
 206	private MessageGenerator mMessageGenerator = new MessageGenerator(this);
 207	private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
 208	private List<Account> accounts;
 209	private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
 210			this);
 211	private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
 212			this);
 213	private AvatarService mAvatarService = new AvatarService(this);
 214	private final List<String> mInProgressAvatarFetches = new ArrayList<>();
 215	private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this);
 216	private OnConversationUpdate mOnConversationUpdate = null;
 217	private Integer convChangedListenerCount = 0;
 218	private OnAccountUpdate mOnAccountUpdate = null;
 219	private OnStatusChanged statusListener = new OnStatusChanged() {
 220
 221		@Override
 222		public void onStatusChanged(Account account) {
 223			XmppConnection connection = account.getXmppConnection();
 224			if (mOnAccountUpdate != null) {
 225				mOnAccountUpdate.onAccountUpdate();
 226			}
 227			if (account.getStatus() == Account.State.ONLINE) {
 228				for (Conversation conversation : account.pendingConferenceLeaves) {
 229					leaveMuc(conversation);
 230				}
 231				for (Conversation conversation : account.pendingConferenceJoins) {
 232					joinMuc(conversation);
 233				}
 234				mMessageArchiveService.executePendingQueries(account);
 235				mJingleConnectionManager.cancelInTransmission();
 236				List<Conversation> conversations = getConversations();
 237				for (Conversation conversation : conversations) {
 238					if (conversation.getAccount() == account) {
 239						conversation.startOtrIfNeeded();
 240						sendUnsentMessages(conversation);
 241					}
 242				}
 243				if (connection != null && connection.getFeatures().csi()) {
 244					if (checkListeners()) {
 245						Log.d(Config.LOGTAG, account.getJid().toBareJid()
 246								+ " sending csi//inactive");
 247						connection.sendInactive();
 248					} else {
 249						Log.d(Config.LOGTAG, account.getJid().toBareJid()
 250								+ " sending csi//active");
 251						connection.sendActive();
 252					}
 253				}
 254				syncDirtyContacts(account);
 255				scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode());
 256			} else if (account.getStatus() == Account.State.OFFLINE) {
 257				resetSendingToWaiting(account);
 258				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 259					int timeToReconnect = mRandom.nextInt(50) + 10;
 260					scheduleWakeUpCall(timeToReconnect,account.getUuid().hashCode());
 261				}
 262			} else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
 263				databaseBackend.updateAccount(account);
 264				reconnectAccount(account, true);
 265			} else if ((account.getStatus() != Account.State.CONNECTING)
 266					&& (account.getStatus() != Account.State.NO_INTERNET)) {
 267				if (connection != null) {
 268					int next = connection.getTimeToNextAttempt();
 269					Log.d(Config.LOGTAG, account.getJid().toBareJid()
 270							+ ": error connecting account. try again in "
 271							+ next + "s for the "
 272							+ (connection.getAttempt() + 1) + " time");
 273					scheduleWakeUpCall(next,account.getUuid().hashCode());
 274				}
 275					}
 276			getNotificationService().updateErrorNotification();
 277		}
 278	};
 279	private int accountChangedListenerCount = 0;
 280	private OnRosterUpdate mOnRosterUpdate = null;
 281	private OnUpdateBlocklist mOnUpdateBlocklist = null;
 282	private int updateBlocklistListenerCount = 0;
 283	private int rosterChangedListenerCount = 0;
 284	private OnMucRosterUpdate mOnMucRosterUpdate = null;
 285	private int mucRosterChangedListenerCount = 0;
 286	private SecureRandom mRandom;
 287	private OpenPgpServiceConnection pgpServiceConnection;
 288	private PgpEngine mPgpEngine = null;
 289	private WakeLock wakeLock;
 290	private PowerManager pm;
 291	private LruCache<String, Bitmap> mBitmapCache;
 292	private Thread mPhoneContactMergerThread;
 293
 294	private boolean mRestoredFromDatabase = false;
 295	public boolean areMessagesInitialized() {
 296		return this.mRestoredFromDatabase;
 297	}
 298
 299	public PgpEngine getPgpEngine() {
 300		if (pgpServiceConnection.isBound()) {
 301			if (this.mPgpEngine == null) {
 302				this.mPgpEngine = new PgpEngine(new OpenPgpApi(
 303							getApplicationContext(),
 304							pgpServiceConnection.getService()), this);
 305			}
 306			return mPgpEngine;
 307		} else {
 308			return null;
 309		}
 310
 311	}
 312
 313	public FileBackend getFileBackend() {
 314		return this.fileBackend;
 315	}
 316
 317	public AvatarService getAvatarService() {
 318		return this.mAvatarService;
 319	}
 320
 321	public void attachLocationToConversation(final Conversation conversation,
 322											 final Uri uri,
 323											 final UiCallback<Message> callback) {
 324		int encryption = conversation.getNextEncryption(forceEncryption());
 325		if (encryption == Message.ENCRYPTION_PGP) {
 326			encryption = Message.ENCRYPTION_DECRYPTED;
 327		}
 328		Message message = new Message(conversation,uri.toString(),encryption);
 329		if (conversation.getNextCounterpart() != null) {
 330			message.setCounterpart(conversation.getNextCounterpart());
 331		}
 332		if (encryption == Message.ENCRYPTION_DECRYPTED) {
 333			getPgpEngine().encrypt(message, callback);
 334		} else {
 335			callback.success(message);
 336		}
 337	}
 338
 339	public void attachFileToConversation(final Conversation conversation,
 340			final Uri uri,
 341			final UiCallback<Message> callback) {
 342		final Message message;
 343		if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
 344			message = new Message(conversation, "",
 345					Message.ENCRYPTION_DECRYPTED);
 346		} else {
 347			message = new Message(conversation, "",
 348					conversation.getNextEncryption(forceEncryption()));
 349		}
 350		message.setCounterpart(conversation.getNextCounterpart());
 351		message.setType(Message.TYPE_FILE);
 352		message.setStatus(Message.STATUS_OFFERED);
 353		String path = getFileBackend().getOriginalPath(uri);
 354		if (path!=null) {
 355			message.setRelativeFilePath(path);
 356			getFileBackend().updateFileParams(message);
 357			if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 358				getPgpEngine().encrypt(message, callback);
 359			} else {
 360				callback.success(message);
 361			}
 362		} else {
 363			new Thread(new Runnable() {
 364				@Override
 365				public void run() {
 366					try {
 367						getFileBackend().copyFileToPrivateStorage(message, uri);
 368						getFileBackend().updateFileParams(message);
 369						if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 370							getPgpEngine().encrypt(message, callback);
 371						} else {
 372							callback.success(message);
 373						}
 374					} catch (FileBackend.FileCopyException e) {
 375						callback.error(e.getResId(),message);
 376					}
 377				}
 378			}).start();
 379
 380		}
 381	}
 382
 383	public void attachImageToConversation(final Conversation conversation,
 384			final Uri uri, final UiCallback<Message> callback) {
 385		final Message message;
 386		if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
 387			message = new Message(conversation, "",
 388					Message.ENCRYPTION_DECRYPTED);
 389		} else {
 390			message = new Message(conversation, "",
 391					conversation.getNextEncryption(forceEncryption()));
 392		}
 393		message.setCounterpart(conversation.getNextCounterpart());
 394		message.setType(Message.TYPE_IMAGE);
 395		message.setStatus(Message.STATUS_OFFERED);
 396		new Thread(new Runnable() {
 397
 398			@Override
 399			public void run() {
 400				try {
 401					getFileBackend().copyImageToPrivateStorage(message, uri);
 402					if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
 403						getPgpEngine().encrypt(message, callback);
 404					} else {
 405						callback.success(message);
 406					}
 407				} catch (final FileBackend.FileCopyException e) {
 408					callback.error(e.getResId(), message);
 409				}
 410			}
 411		}).start();
 412	}
 413
 414	public Conversation find(Bookmark bookmark) {
 415		return find(bookmark.getAccount(), bookmark.getJid());
 416	}
 417
 418	public Conversation find(final Account account, final Jid jid) {
 419		return find(getConversations(), account, jid);
 420	}
 421
 422	@Override
 423	public int onStartCommand(Intent intent, int flags, int startId) {
 424		final String action = intent == null ? null : intent.getAction();
 425		if (action != null) {
 426			switch (action) {
 427				case ACTION_MERGE_PHONE_CONTACTS:
 428					if (mRestoredFromDatabase) {
 429						PhoneHelper.loadPhoneContacts(getApplicationContext(),
 430								new CopyOnWriteArrayList<Bundle>(),
 431								this);
 432					}
 433					return START_STICKY;
 434				case Intent.ACTION_SHUTDOWN:
 435					logoutAndSave();
 436					return START_NOT_STICKY;
 437				case ACTION_CLEAR_NOTIFICATION:
 438					mNotificationService.clear();
 439					break;
 440				case ACTION_DISABLE_FOREGROUND:
 441					getPreferences().edit().putBoolean("keep_foreground_service",false).commit();
 442					toggleForegroundService();
 443					break;
 444				case ACTION_TRY_AGAIN:
 445					for(Account account : accounts) {
 446						if (account.hasErrorStatus()) {
 447							final XmppConnection connection = account.getXmppConnection();
 448							if (connection != null) {
 449								connection.resetAttemptCount();
 450							}
 451						}
 452					}
 453					break;
 454				case ACTION_DISABLE_ACCOUNT:
 455					try {
 456						String jid = intent.getStringExtra("account");
 457						Account account = jid == null ? null : findAccountByJid(Jid.fromString(jid));
 458						if (account != null) {
 459							account.setOption(Account.OPTION_DISABLED,true);
 460							updateAccount(account);
 461						}
 462					} catch (final InvalidJidException ignored) {
 463						break;
 464					}
 465					break;
 466			}
 467		}
 468		this.wakeLock.acquire();
 469
 470		for (Account account : accounts) {
 471			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
 472				if (!hasInternetConnection()) {
 473					account.setStatus(Account.State.NO_INTERNET);
 474					if (statusListener != null) {
 475						statusListener.onStatusChanged(account);
 476					}
 477				} else {
 478					if (account.getStatus() == Account.State.NO_INTERNET) {
 479						account.setStatus(Account.State.OFFLINE);
 480						if (statusListener != null) {
 481							statusListener.onStatusChanged(account);
 482						}
 483					}
 484					if (account.getStatus() == Account.State.ONLINE) {
 485						long lastReceived = account.getXmppConnection().getLastPacketReceived();
 486						long lastSent = account.getXmppConnection().getLastPingSent();
 487						long pingInterval = "ui".equals(action) ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
 488						long msToNextPing = (Math.max(lastReceived,lastSent) + pingInterval) - SystemClock.elapsedRealtime();
 489						if (lastSent > lastReceived && (lastSent +  Config.PING_TIMEOUT * 1000) < SystemClock.elapsedRealtime()) {
 490							Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": ping timeout");
 491							this.reconnectAccount(account, true);
 492						} else if (msToNextPing <= 0) {
 493							account.getXmppConnection().sendPing();
 494							Log.d(Config.LOGTAG, account.getJid().toBareJid()+" send ping");
 495							this.scheduleWakeUpCall(Config.PING_TIMEOUT,account.getUuid().hashCode());
 496						} else {
 497							this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
 498						}
 499					} else if (account.getStatus() == Account.State.OFFLINE) {
 500						reconnectAccount(account,true);
 501					} else if (account.getStatus() == Account.State.CONNECTING) {
 502						long timeout = Config.CONNECT_TIMEOUT - ((SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000);
 503						if (timeout < 0) {
 504							Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting");
 505							reconnectAccount(account, true);
 506						} else {
 507							scheduleWakeUpCall((int) timeout,account.getUuid().hashCode());
 508						}
 509					} else {
 510						if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
 511							reconnectAccount(account, true);
 512						}
 513					}
 514
 515				}
 516				if (mOnAccountUpdate != null) {
 517					mOnAccountUpdate.onAccountUpdate();
 518				}
 519			}
 520		}
 521		/*PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE);
 522			if (!pm.isScreenOn()) {
 523			removeStaleListeners();
 524			}*/
 525		if (wakeLock.isHeld()) {
 526			try {
 527				wakeLock.release();
 528			} catch (final RuntimeException ignored) {
 529			}
 530		}
 531		return START_STICKY;
 532	}
 533
 534	public boolean hasInternetConnection() {
 535		ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
 536			.getSystemService(Context.CONNECTIVITY_SERVICE);
 537		NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
 538		return activeNetwork != null && activeNetwork.isConnected();
 539	}
 540
 541	@SuppressLint("TrulyRandom")
 542	@Override
 543	public void onCreate() {
 544		ExceptionHelper.init(getApplicationContext());
 545		PRNGFixes.apply();
 546		this.mRandom = new SecureRandom();
 547		updateMemorizingTrustmanager();
 548		final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
 549		final int cacheSize = maxMemory / 8;
 550		this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
 551			@Override
 552			protected int sizeOf(final String key, final Bitmap bitmap) {
 553				return bitmap.getByteCount() / 1024;
 554			}
 555		};
 556
 557		this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
 558		this.accounts = databaseBackend.getAccounts();
 559
 560		for (final Account account : this.accounts) {
 561			account.initOtrEngine(this);
 562		}
 563		restoreFromDatabase();
 564
 565		getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
 566		this.fileObserver.startWatching();
 567		this.pgpServiceConnection = new OpenPgpServiceConnection(getApplicationContext(), "org.sufficientlysecure.keychain");
 568		this.pgpServiceConnection.bindToService();
 569
 570		this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 571		this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,"XmppConnectionService");
 572		toggleForegroundService();
 573	}
 574
 575	public void toggleForegroundService() {
 576		if (getPreferences().getBoolean("keep_foreground_service",false)) {
 577			startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
 578		} else {
 579			stopForeground(true);
 580		}
 581	}
 582
 583	@Override
 584	public void onTaskRemoved(final Intent rootIntent) {
 585		super.onTaskRemoved(rootIntent);
 586		if (!getPreferences().getBoolean("keep_foreground_service",false)) {
 587			this.logoutAndSave();
 588		}
 589	}
 590
 591	private void logoutAndSave() {
 592		for (final Account account : accounts) {
 593			databaseBackend.writeRoster(account.getRoster());
 594			if (account.getXmppConnection() != null) {
 595				disconnect(account, false);
 596			}
 597		}
 598		Context context = getApplicationContext();
 599		AlarmManager alarmManager = (AlarmManager) context
 600			.getSystemService(Context.ALARM_SERVICE);
 601		Intent intent = new Intent(context, EventReceiver.class);
 602		alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0));
 603		Log.d(Config.LOGTAG, "good bye");
 604		stopSelf();
 605	}
 606
 607	protected void scheduleWakeUpCall(int seconds, int requestCode) {
 608		final long timeToWake = SystemClock.elapsedRealtime() + (seconds + 1) * 1000;
 609
 610		Context context = getApplicationContext();
 611		AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
 612
 613		Intent intent = new Intent(context, EventReceiver.class);
 614		intent.setAction("ping");
 615		PendingIntent alarmIntent = PendingIntent.getBroadcast(context, requestCode, intent, 0);
 616		alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, timeToWake, alarmIntent);
 617	}
 618
 619	public XmppConnection createConnection(final Account account) {
 620		final SharedPreferences sharedPref = getPreferences();
 621		account.setResource(sharedPref.getString("resource", "mobile")
 622				.toLowerCase(Locale.getDefault()));
 623		final XmppConnection connection = new XmppConnection(account, this);
 624		connection.setOnMessagePacketReceivedListener(this.mMessageParser);
 625		connection.setOnStatusChangedListener(this.statusListener);
 626		connection.setOnPresencePacketReceivedListener(this.mPresenceParser);
 627		connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser);
 628		connection.setOnJinglePacketReceivedListener(this.jingleListener);
 629		connection.setOnBindListener(this.mOnBindListener);
 630		connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener);
 631		connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService);
 632		return connection;
 633	}
 634
 635	public void sendChatState(Conversation conversation) {
 636		if (sendChatStates()) {
 637			MessagePacket packet = mMessageGenerator.generateChatState(conversation);
 638			sendMessagePacket(conversation.getAccount(), packet);
 639		}
 640	}
 641
 642	public void sendMessage(final Message message) {
 643		final Account account = message.getConversation().getAccount();
 644		account.deactivateGracePeriod();
 645		final Conversation conv = message.getConversation();
 646		MessagePacket packet = null;
 647		boolean saveInDb = true;
 648		boolean send = false;
 649		if (account.getStatus() == Account.State.ONLINE
 650				&& account.getXmppConnection() != null) {
 651			if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
 652				if (message.getCounterpart() != null) {
 653					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 654						if (!conv.hasValidOtrSession()) {
 655							conv.startOtrSession(message.getCounterpart().getResourcepart(),true);
 656							message.setStatus(Message.STATUS_WAITING);
 657						} else if (conv.hasValidOtrSession()
 658								&& conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 659							mJingleConnectionManager
 660								.createNewConnection(message);
 661								}
 662					} else {
 663						mJingleConnectionManager.createNewConnection(message);
 664					}
 665				} else {
 666					if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 667						conv.startOtrIfNeeded();
 668					}
 669					message.setStatus(Message.STATUS_WAITING);
 670				}
 671			} else {
 672				if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 673					if (!conv.hasValidOtrSession() && (message.getCounterpart() != null)) {
 674						conv.startOtrSession(message.getCounterpart().getResourcepart(), true);
 675						message.setStatus(Message.STATUS_WAITING);
 676					} else if (conv.hasValidOtrSession()) {
 677						if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
 678							packet = mMessageGenerator.generateOtrChat(message);
 679							send = true;
 680						} else {
 681							message.setStatus(Message.STATUS_WAITING);
 682							conv.startOtrIfNeeded();
 683						}
 684					} else {
 685						message.setStatus(Message.STATUS_WAITING);
 686					}
 687				} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 688					message.getConversation().endOtrIfNeeded();
 689					message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
 690						@Override
 691						public void onMessageFound(Message message) {
 692							markMessage(message,Message.STATUS_SEND_FAILED);
 693						}
 694					});
 695					packet = mMessageGenerator.generatePgpChat(message);
 696					send = true;
 697				} else {
 698					message.getConversation().endOtrIfNeeded();
 699					message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
 700						@Override
 701						public void onMessageFound(Message message) {
 702							markMessage(message,Message.STATUS_SEND_FAILED);
 703						}
 704					});
 705					packet = mMessageGenerator.generateChat(message);
 706					send = true;
 707				}
 708			}
 709			if (!account.getXmppConnection().getFeatures().sm()
 710					&& conv.getMode() != Conversation.MODE_MULTI) {
 711				message.setStatus(Message.STATUS_SEND);
 712					}
 713		} else {
 714			message.setStatus(Message.STATUS_WAITING);
 715			if (message.getType() == Message.TYPE_TEXT) {
 716				if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
 717					String pgpBody = message.getEncryptedBody();
 718					String decryptedBody = message.getBody();
 719					message.setBody(pgpBody);
 720					message.setEncryption(Message.ENCRYPTION_PGP);
 721					databaseBackend.createMessage(message);
 722					saveInDb = false;
 723					message.setBody(decryptedBody);
 724					message.setEncryption(Message.ENCRYPTION_DECRYPTED);
 725				} else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 726					if (!conv.hasValidOtrSession()
 727							&& message.getCounterpart() != null) {
 728						conv.startOtrSession(message.getCounterpart().getResourcepart(), false);
 729							}
 730				}
 731			}
 732
 733		}
 734		conv.add(message);
 735		if (saveInDb) {
 736			if (message.getEncryption() == Message.ENCRYPTION_NONE
 737					|| saveEncryptedMessages()) {
 738				databaseBackend.createMessage(message);
 739					}
 740		}
 741		if ((send) && (packet != null)) {
 742			if (conv.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
 743				if (this.sendChatStates()) {
 744					packet.addChild(ChatState.toElement(conv.getOutgoingChatState()));
 745				}
 746			}
 747			sendMessagePacket(account, packet);
 748		}
 749		updateConversationUi();
 750	}
 751
 752	private void sendUnsentMessages(final Conversation conversation) {
 753		conversation.findWaitingMessages(new Conversation.OnMessageFound() {
 754
 755			@Override
 756			public void onMessageFound(Message message) {
 757				resendMessage(message);
 758			}
 759		});
 760	}
 761
 762	private void resendMessage(final Message message) {
 763		Account account = message.getConversation().getAccount();
 764		MessagePacket packet = null;
 765		if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 766			Presences presences = message.getConversation().getContact()
 767				.getPresences();
 768			if (!message.getConversation().hasValidOtrSession()) {
 769				if ((message.getCounterpart() != null)
 770						&& (presences.has(message.getCounterpart().getResourcepart()))) {
 771					message.getConversation().startOtrSession(message.getCounterpart().getResourcepart(), true);
 772				} else {
 773					if (presences.size() == 1) {
 774						String presence = presences.asStringArray()[0];
 775						message.getConversation().startOtrSession(presence, true);
 776					}
 777				}
 778			} else {
 779				if (message.getConversation().getOtrSession()
 780						.getSessionStatus() == SessionStatus.ENCRYPTED) {
 781					try {
 782						message.setCounterpart(Jid.fromSessionID(message.getConversation().getOtrSession().getSessionID()));
 783						if (message.getType() == Message.TYPE_TEXT) {
 784							packet = mMessageGenerator.generateOtrChat(message,
 785									true);
 786						} else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
 787							mJingleConnectionManager.createNewConnection(message);
 788						}
 789					} catch (final InvalidJidException ignored) {
 790
 791					}
 792						}
 793			}
 794		} else if (message.getType() == Message.TYPE_TEXT) {
 795			if (message.getEncryption() == Message.ENCRYPTION_NONE) {
 796				packet = mMessageGenerator.generateChat(message, true);
 797			} else if ((message.getEncryption() == Message.ENCRYPTION_DECRYPTED)
 798					|| (message.getEncryption() == Message.ENCRYPTION_PGP)) {
 799				packet = mMessageGenerator.generatePgpChat(message, true);
 800					}
 801		} else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
 802			Contact contact = message.getConversation().getContact();
 803			Presences presences = contact.getPresences();
 804			if ((message.getCounterpart() != null)
 805					&& (presences.has(message.getCounterpart().getResourcepart()))) {
 806				markMessage(message, Message.STATUS_OFFERED);
 807				mJingleConnectionManager.createNewConnection(message);
 808			} else {
 809				if (presences.size() == 1) {
 810					String presence = presences.asStringArray()[0];
 811					try {
 812						message.setCounterpart(Jid.fromParts(contact.getJid().getLocalpart(), contact.getJid().getDomainpart(), presence));
 813					} catch (InvalidJidException e) {
 814						return;
 815					}
 816					markMessage(message, Message.STATUS_OFFERED);
 817					mJingleConnectionManager.createNewConnection(message);
 818				}
 819			}
 820		}
 821		if (packet != null) {
 822			if (!account.getXmppConnection().getFeatures().sm()
 823					&& message.getConversation().getMode() != Conversation.MODE_MULTI) {
 824				markMessage(message, Message.STATUS_SEND);
 825			} else {
 826				markMessage(message, Message.STATUS_UNSEND);
 827			}
 828			if (message.getConversation().setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
 829				if (this.sendChatStates()) {
 830					packet.addChild(ChatState.toElement(message.getConversation().getOutgoingChatState()));
 831				}
 832			}
 833			sendMessagePacket(account, packet);
 834		}
 835	}
 836
 837	public void fetchRosterFromServer(final Account account) {
 838		final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
 839		if (!"".equals(account.getRosterVersion())) {
 840			Log.d(Config.LOGTAG, account.getJid().toBareJid()
 841					+ ": fetching roster version " + account.getRosterVersion());
 842		} else {
 843			Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
 844		}
 845		iqPacket.query(Xmlns.ROSTER).setAttribute("ver",
 846				account.getRosterVersion());
 847		account.getXmppConnection().sendIqPacket(iqPacket, mIqParser);
 848	}
 849
 850	public void fetchBookmarks(final Account account) {
 851		final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET);
 852		final Element query = iqPacket.query("jabber:iq:private");
 853		query.addChild("storage", "storage:bookmarks");
 854		final OnIqPacketReceived callback = new OnIqPacketReceived() {
 855
 856			@Override
 857			public void onIqPacketReceived(final Account account, final IqPacket packet) {
 858				final Element query = packet.query();
 859				final List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
 860				final Element storage = query.findChild("storage",
 861						"storage:bookmarks");
 862				if (storage != null) {
 863					for (final Element item : storage.getChildren()) {
 864						if (item.getName().equals("conference")) {
 865							final Bookmark bookmark = Bookmark.parse(item, account);
 866							bookmarks.add(bookmark);
 867							Conversation conversation = find(bookmark);
 868							if (conversation != null) {
 869								conversation.setBookmark(bookmark);
 870							} else if (bookmark.autojoin() && bookmark.getJid() != null) {
 871								conversation = findOrCreateConversation(
 872										account, bookmark.getJid(), true);
 873								conversation.setBookmark(bookmark);
 874								joinMuc(conversation);
 875							}
 876						}
 877					}
 878				}
 879				account.setBookmarks(bookmarks);
 880			}
 881		};
 882		sendIqPacket(account, iqPacket, callback);
 883	}
 884
 885	public void pushBookmarks(Account account) {
 886		IqPacket iqPacket = new IqPacket(IqPacket.TYPE.SET);
 887		Element query = iqPacket.query("jabber:iq:private");
 888		Element storage = query.addChild("storage", "storage:bookmarks");
 889		for (Bookmark bookmark : account.getBookmarks()) {
 890			storage.addChild(bookmark);
 891		}
 892		sendIqPacket(account, iqPacket, null);
 893	}
 894
 895	public void onPhoneContactsLoaded(final List<Bundle> phoneContacts) {
 896		if (mPhoneContactMergerThread != null) {
 897			mPhoneContactMergerThread.interrupt();
 898		}
 899		mPhoneContactMergerThread = new Thread(new Runnable() {
 900			@Override
 901			public void run() {
 902				Log.d(Config.LOGTAG,"start merging phone contacts with roster");
 903				for (Account account : accounts) {
 904					account.getRoster().clearSystemAccounts();
 905					for (Bundle phoneContact : phoneContacts) {
 906						if (Thread.interrupted()) {
 907							Log.d(Config.LOGTAG,"interrupted merging phone contacts");
 908							return;
 909						}
 910						Jid jid;
 911						try {
 912							jid = Jid.fromString(phoneContact.getString("jid"));
 913						} catch (final InvalidJidException e) {
 914							continue;
 915						}
 916						final Contact contact = account.getRoster().getContact(jid);
 917						String systemAccount = phoneContact.getInt("phoneid")
 918							+ "#"
 919							+ phoneContact.getString("lookup");
 920						contact.setSystemAccount(systemAccount);
 921						contact.setPhotoUri(phoneContact.getString("photouri"));
 922						getAvatarService().clear(contact);
 923						contact.setSystemName(phoneContact.getString("displayname"));
 924					}
 925				}
 926				Log.d(Config.LOGTAG,"finished merging phone contacts");
 927				updateAccountUi();
 928			}
 929		});
 930		mPhoneContactMergerThread.start();
 931	}
 932
 933	private void restoreFromDatabase() {
 934		synchronized (this.conversations) {
 935			final Map<String, Account> accountLookupTable = new Hashtable<>();
 936			for (Account account : this.accounts) {
 937				accountLookupTable.put(account.getUuid(), account);
 938			}
 939			this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE));
 940			for (Conversation conversation : this.conversations) {
 941				Account account = accountLookupTable.get(conversation.getAccountUuid());
 942				conversation.setAccount(account);
 943			}
 944			new Thread(new Runnable() {
 945				@Override
 946				public void run() {
 947					Log.d(Config.LOGTAG,"restoring roster");
 948					for(Account account : accounts) {
 949						databaseBackend.readRoster(account.getRoster());
 950					}
 951					getBitmapCache().evictAll();
 952					Looper.prepare();
 953					PhoneHelper.loadPhoneContacts(getApplicationContext(),
 954							new CopyOnWriteArrayList<Bundle>(),
 955							XmppConnectionService.this);
 956					Log.d(Config.LOGTAG,"restoring messages");
 957					for (Conversation conversation : conversations) {
 958						conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
 959						checkDeletedFiles(conversation);
 960					}
 961					mRestoredFromDatabase = true;
 962					Log.d(Config.LOGTAG,"restored all messages");
 963					updateConversationUi();
 964				}
 965			}).start();
 966		}
 967	}
 968
 969	public List<Conversation> getConversations() {
 970		return this.conversations;
 971	}
 972
 973	private void checkDeletedFiles(Conversation conversation) {
 974		conversation.findMessagesWithFiles(new Conversation.OnMessageFound() {
 975
 976			@Override
 977			public void onMessageFound(Message message) {
 978				if (!getFileBackend().isFileAvailable(message)) {
 979					message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED));
 980				}
 981			}
 982		});
 983	}
 984
 985	private void markFileDeleted(String uuid) {
 986		for (Conversation conversation : getConversations()) {
 987			Message message = conversation.findMessageWithFileAndUuid(uuid);
 988			if (message != null) {
 989				if (!getFileBackend().isFileAvailable(message)) {
 990					message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED));
 991					updateConversationUi();
 992				}
 993				return;
 994			}
 995		}
 996	}
 997
 998	public void populateWithOrderedConversations(final List<Conversation> list) {
 999		populateWithOrderedConversations(list, true);
1000	}
1001
1002	public void populateWithOrderedConversations(final List<Conversation> list, boolean includeConferences) {
1003		list.clear();
1004		if (includeConferences) {
1005			list.addAll(getConversations());
1006		} else {
1007			for (Conversation conversation : getConversations()) {
1008				if (conversation.getMode() == Conversation.MODE_SINGLE) {
1009					list.add(conversation);
1010				}
1011			}
1012		}
1013		Collections.sort(list, new Comparator<Conversation>() {
1014			@Override
1015			public int compare(Conversation lhs, Conversation rhs) {
1016				Message left = lhs.getLatestMessage();
1017				Message right = rhs.getLatestMessage();
1018				if (left.getTimeSent() > right.getTimeSent()) {
1019					return -1;
1020				} else if (left.getTimeSent() < right.getTimeSent()) {
1021					return 1;
1022				} else {
1023					return 0;
1024				}
1025			}
1026		});
1027	}
1028
1029	public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) {
1030		Log.d(Config.LOGTAG,"load more messages for "+conversation.getName() + " prior to "+MessageGenerator.getTimestamp(timestamp));
1031		if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation,callback)) {
1032			return;
1033		}
1034		new Thread(new Runnable() {
1035			@Override
1036			public void run() {
1037				final Account account = conversation.getAccount();
1038				List<Message> messages = databaseBackend.getMessages(conversation, 50,timestamp);
1039				if (messages.size() > 0) {
1040					conversation.addAll(0, messages);
1041					checkDeletedFiles(conversation);
1042					callback.onMoreMessagesLoaded(messages.size(), conversation);
1043				} else if (conversation.hasMessagesLeftOnServer()
1044						&& account.isOnlineAndConnected()
1045						&& account.getXmppConnection().getFeatures().mam()) {
1046					MessageArchiveService.Query query = getMessageArchiveService().query(conversation,0,timestamp - 1);
1047					if (query != null) {
1048						query.setCallback(callback);
1049					}
1050					callback.informUser(R.string.fetching_history_from_server);
1051				}
1052			}
1053		}).start();
1054	}
1055
1056	public List<Account> getAccounts() {
1057		return this.accounts;
1058	}
1059
1060	public Conversation find(final Iterable<Conversation> haystack, final Contact contact) {
1061		for (final Conversation conversation : haystack) {
1062			if (conversation.getContact() == contact) {
1063				return conversation;
1064			}
1065		}
1066		return null;
1067	}
1068
1069	public Conversation find(final Iterable<Conversation> haystack, final Account account, final Jid jid) {
1070		if (jid == null) {
1071			return null;
1072		}
1073		for (final Conversation conversation : haystack) {
1074			if ((account == null || conversation.getAccount() == account)
1075					&& (conversation.getJid().toBareJid().equals(jid.toBareJid()))) {
1076				return conversation;
1077			}
1078		}
1079		return null;
1080	}
1081
1082	public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc) {
1083		return this.findOrCreateConversation(account, jid, muc, null);
1084	}
1085
1086	public Conversation findOrCreateConversation(final Account account, final Jid jid, final boolean muc, final MessageArchiveService.Query query) {
1087		synchronized (this.conversations) {
1088			Conversation conversation = find(account, jid);
1089			if (conversation != null) {
1090				return conversation;
1091			}
1092			conversation = databaseBackend.findConversation(account, jid);
1093			if (conversation != null) {
1094				conversation.setStatus(Conversation.STATUS_AVAILABLE);
1095				conversation.setAccount(account);
1096				if (muc) {
1097					conversation.setMode(Conversation.MODE_MULTI);
1098					conversation.setContactJid(jid);
1099				} else {
1100					conversation.setMode(Conversation.MODE_SINGLE);
1101					conversation.setContactJid(jid.toBareJid());
1102				}
1103				conversation.setNextEncryption(-1);
1104				conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE));
1105				this.databaseBackend.updateConversation(conversation);
1106			} else {
1107				String conversationName;
1108				Contact contact = account.getRoster().getContact(jid);
1109				if (contact != null) {
1110					conversationName = contact.getDisplayName();
1111				} else {
1112					conversationName = jid.getLocalpart();
1113				}
1114				if (muc) {
1115					conversation = new Conversation(conversationName, account, jid,
1116							Conversation.MODE_MULTI);
1117				} else {
1118					conversation = new Conversation(conversationName, account, jid.toBareJid(),
1119							Conversation.MODE_SINGLE);
1120				}
1121				this.databaseBackend.createConversation(conversation);
1122			}
1123			if (account.getXmppConnection() != null
1124					&& account.getXmppConnection().getFeatures().mam()
1125					&& !muc) {
1126				if (query == null) {
1127					this.mMessageArchiveService.query(conversation);
1128				} else {
1129					if (query.getConversation() == null) {
1130						this.mMessageArchiveService.query(conversation, query.getStart());
1131					}
1132				}
1133			}
1134			checkDeletedFiles(conversation);
1135			this.conversations.add(conversation);
1136			updateConversationUi();
1137			return conversation;
1138		}
1139	}
1140
1141	public void archiveConversation(Conversation conversation) {
1142		getNotificationService().clear(conversation);
1143		conversation.setStatus(Conversation.STATUS_ARCHIVED);
1144		conversation.setNextEncryption(-1);
1145		synchronized (this.conversations) {
1146			if (conversation.getMode() == Conversation.MODE_MULTI) {
1147				if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1148					Bookmark bookmark = conversation.getBookmark();
1149					if (bookmark != null && bookmark.autojoin()) {
1150						bookmark.setAutojoin(false);
1151						pushBookmarks(bookmark.getAccount());
1152					}
1153				}
1154				leaveMuc(conversation);
1155			} else {
1156				conversation.endOtrIfNeeded();
1157			}
1158			this.databaseBackend.updateConversation(conversation);
1159			this.conversations.remove(conversation);
1160			updateConversationUi();
1161		}
1162	}
1163
1164	public void createAccount(final Account account) {
1165		account.initOtrEngine(this);
1166		databaseBackend.createAccount(account);
1167		this.accounts.add(account);
1168		this.reconnectAccountInBackground(account);
1169		updateAccountUi();
1170	}
1171
1172	public void updateAccount(final Account account) {
1173		this.statusListener.onStatusChanged(account);
1174		databaseBackend.updateAccount(account);
1175		reconnectAccount(account, false);
1176		updateAccountUi();
1177		getNotificationService().updateErrorNotification();
1178	}
1179
1180	public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) {
1181		final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword);
1182		sendIqPacket(account, iq, new OnIqPacketReceived() {
1183			@Override
1184			public void onIqPacketReceived(final Account account, final IqPacket packet) {
1185				if (packet.getType() == IqPacket.TYPE.RESULT) {
1186					account.setPassword(newPassword);
1187					databaseBackend.updateAccount(account);
1188					callback.onPasswordChangeSucceeded();
1189				} else {
1190					callback.onPasswordChangeFailed();
1191				}
1192			}
1193		});
1194	}
1195
1196	public void deleteAccount(final Account account) {
1197		synchronized (this.conversations) {
1198			for (final Conversation conversation : conversations) {
1199				if (conversation.getAccount() == account) {
1200					if (conversation.getMode() == Conversation.MODE_MULTI) {
1201						leaveMuc(conversation);
1202					} else if (conversation.getMode() == Conversation.MODE_SINGLE) {
1203						conversation.endOtrIfNeeded();
1204					}
1205					conversations.remove(conversation);
1206				}
1207			}
1208			if (account.getXmppConnection() != null) {
1209				this.disconnect(account, true);
1210			}
1211			databaseBackend.deleteAccount(account);
1212			this.accounts.remove(account);
1213			updateAccountUi();
1214			getNotificationService().updateErrorNotification();
1215		}
1216	}
1217
1218	public void setOnConversationListChangedListener(OnConversationUpdate listener) {
1219		synchronized (this) {
1220			if (checkListeners()) {
1221				switchToForeground();
1222			}
1223			this.mOnConversationUpdate = listener;
1224			this.mNotificationService.setIsInForeground(true);
1225			if (this.convChangedListenerCount < 2) {
1226				this.convChangedListenerCount++;
1227			}
1228		}
1229	}
1230
1231	public void removeOnConversationListChangedListener() {
1232		synchronized (this) {
1233			this.convChangedListenerCount--;
1234			if (this.convChangedListenerCount <= 0) {
1235				this.convChangedListenerCount = 0;
1236				this.mOnConversationUpdate = null;
1237				this.mNotificationService.setIsInForeground(false);
1238				if (checkListeners()) {
1239					switchToBackground();
1240				}
1241			}
1242		}
1243	}
1244
1245	public void setOnAccountListChangedListener(OnAccountUpdate listener) {
1246		synchronized (this) {
1247			if (checkListeners()) {
1248				switchToForeground();
1249			}
1250			this.mOnAccountUpdate = listener;
1251			if (this.accountChangedListenerCount < 2) {
1252				this.accountChangedListenerCount++;
1253			}
1254		}
1255	}
1256
1257	public void removeOnAccountListChangedListener() {
1258		synchronized (this) {
1259			this.accountChangedListenerCount--;
1260			if (this.accountChangedListenerCount <= 0) {
1261				this.mOnAccountUpdate = null;
1262				this.accountChangedListenerCount = 0;
1263				if (checkListeners()) {
1264					switchToBackground();
1265				}
1266			}
1267		}
1268	}
1269
1270	public void setOnRosterUpdateListener(final OnRosterUpdate listener) {
1271		synchronized (this) {
1272			if (checkListeners()) {
1273				switchToForeground();
1274			}
1275			this.mOnRosterUpdate = listener;
1276			if (this.rosterChangedListenerCount < 2) {
1277				this.rosterChangedListenerCount++;
1278			}
1279		}
1280	}
1281
1282	public void removeOnRosterUpdateListener() {
1283		synchronized (this) {
1284			this.rosterChangedListenerCount--;
1285			if (this.rosterChangedListenerCount <= 0) {
1286				this.rosterChangedListenerCount = 0;
1287				this.mOnRosterUpdate = null;
1288				if (checkListeners()) {
1289					switchToBackground();
1290				}
1291			}
1292		}
1293	}
1294
1295	public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) {
1296		synchronized (this) {
1297			if (checkListeners()) {
1298				switchToForeground();
1299			}
1300			this.mOnUpdateBlocklist = listener;
1301			if (this.updateBlocklistListenerCount < 2) {
1302				this.updateBlocklistListenerCount++;
1303			}
1304		}
1305	}
1306
1307	public void removeOnUpdateBlocklistListener() {
1308		synchronized (this) {
1309			this.updateBlocklistListenerCount--;
1310			if (this.updateBlocklistListenerCount <= 0) {
1311				this.updateBlocklistListenerCount = 0;
1312				this.mOnUpdateBlocklist = null;
1313				if (checkListeners()) {
1314					switchToBackground();
1315				}
1316			}
1317		}
1318	}
1319
1320	public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
1321		synchronized (this) {
1322			if (checkListeners()) {
1323				switchToForeground();
1324			}
1325			this.mOnMucRosterUpdate = listener;
1326			if (this.mucRosterChangedListenerCount < 2) {
1327				this.mucRosterChangedListenerCount++;
1328			}
1329		}
1330	}
1331
1332	public void removeOnMucRosterUpdateListener() {
1333		synchronized (this) {
1334			this.mucRosterChangedListenerCount--;
1335			if (this.mucRosterChangedListenerCount <= 0) {
1336				this.mucRosterChangedListenerCount = 0;
1337				this.mOnMucRosterUpdate = null;
1338				if (checkListeners()) {
1339					switchToBackground();
1340				}
1341			}
1342		}
1343	}
1344
1345	private boolean checkListeners() {
1346		return (this.mOnAccountUpdate == null
1347				&& this.mOnConversationUpdate == null
1348				&& this.mOnRosterUpdate == null
1349				&& this.mOnUpdateBlocklist == null);
1350	}
1351
1352	private void switchToForeground() {
1353		for (Account account : getAccounts()) {
1354			if (account.getStatus() == Account.State.ONLINE) {
1355				XmppConnection connection = account.getXmppConnection();
1356				if (connection != null && connection.getFeatures().csi()) {
1357					connection.sendActive();
1358				}
1359			}
1360		}
1361		Log.d(Config.LOGTAG, "app switched into foreground");
1362	}
1363
1364	private void switchToBackground() {
1365		for (Account account : getAccounts()) {
1366			if (account.getStatus() == Account.State.ONLINE) {
1367				XmppConnection connection = account.getXmppConnection();
1368				if (connection != null && connection.getFeatures().csi()) {
1369					connection.sendInactive();
1370				}
1371			}
1372		}
1373		for(Conversation conversation : getConversations()) {
1374			conversation.setIncomingChatState(ChatState.ACTIVE);
1375		}
1376		this.mNotificationService.setIsInForeground(false);
1377		Log.d(Config.LOGTAG, "app switched into background");
1378	}
1379
1380	private void connectMultiModeConversations(Account account) {
1381		List<Conversation> conversations = getConversations();
1382		for (Conversation conversation : conversations) {
1383			if ((conversation.getMode() == Conversation.MODE_MULTI)
1384					&& (conversation.getAccount() == account)) {
1385				conversation.resetMucOptions();
1386				joinMuc(conversation);
1387			}
1388		}
1389	}
1390
1391	public void joinMuc(Conversation conversation) {
1392		Account account = conversation.getAccount();
1393		account.pendingConferenceJoins.remove(conversation);
1394		account.pendingConferenceLeaves.remove(conversation);
1395		if (account.getStatus() == Account.State.ONLINE) {
1396			final String nick = conversation.getMucOptions().getProposedNick();
1397			final Jid joinJid = conversation.getMucOptions().createJoinJid(nick);
1398			if (joinJid == null) {
1399				return; //safety net
1400			}
1401			Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": joining conversation " + joinJid.toString());
1402			PresencePacket packet = new PresencePacket();
1403			packet.setFrom(conversation.getAccount().getJid());
1404			packet.setTo(joinJid);
1405			Element x = packet.addChild("x", "http://jabber.org/protocol/muc");
1406			if (conversation.getMucOptions().getPassword() != null) {
1407				x.addChild("password").setContent(conversation.getMucOptions().getPassword());
1408			}
1409			x.addChild("history").setAttribute("since", PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted()));
1410			String sig = account.getPgpSignature();
1411			if (sig != null) {
1412				packet.addChild("status").setContent("online");
1413				packet.addChild("x", "jabber:x:signed").setContent(sig);
1414			}
1415			sendPresencePacket(account, packet);
1416			fetchConferenceConfiguration(conversation);
1417			if (!joinJid.equals(conversation.getJid())) {
1418				conversation.setContactJid(joinJid);
1419				databaseBackend.updateConversation(conversation);
1420			}
1421			conversation.setHasMessagesLeftOnServer(false);
1422		} else {
1423			account.pendingConferenceJoins.add(conversation);
1424		}
1425	}
1426
1427	public void providePasswordForMuc(Conversation conversation, String password) {
1428		if (conversation.getMode() == Conversation.MODE_MULTI) {
1429			conversation.getMucOptions().setPassword(password);
1430			if (conversation.getBookmark() != null) {
1431				conversation.getBookmark().setAutojoin(true);
1432				pushBookmarks(conversation.getAccount());
1433			}
1434			databaseBackend.updateConversation(conversation);
1435			joinMuc(conversation);
1436		}
1437	}
1438
1439	public void renameInMuc(final Conversation conversation, final String nick, final UiCallback<Conversation> callback) {
1440		final MucOptions options = conversation.getMucOptions();
1441		final Jid joinJid = options.createJoinJid(nick);
1442		if (options.online()) {
1443			Account account = conversation.getAccount();
1444			options.setOnRenameListener(new OnRenameListener() {
1445
1446				@Override
1447				public void onSuccess() {
1448					conversation.setContactJid(joinJid);
1449					databaseBackend.updateConversation(conversation);
1450					Bookmark bookmark = conversation.getBookmark();
1451					if (bookmark != null) {
1452						bookmark.setNick(nick);
1453						pushBookmarks(bookmark.getAccount());
1454					}
1455					callback.success(conversation);
1456				}
1457
1458				@Override
1459				public void onFailure() {
1460					callback.error(R.string.nick_in_use, conversation);
1461				}
1462			});
1463
1464			PresencePacket packet = new PresencePacket();
1465			packet.setTo(joinJid);
1466			packet.setFrom(conversation.getAccount().getJid());
1467
1468			String sig = account.getPgpSignature();
1469			if (sig != null) {
1470				packet.addChild("status").setContent("online");
1471				packet.addChild("x", "jabber:x:signed").setContent(sig);
1472			}
1473			sendPresencePacket(account, packet);
1474		} else {
1475			conversation.setContactJid(joinJid);
1476			databaseBackend.updateConversation(conversation);
1477			if (conversation.getAccount().getStatus() == Account.State.ONLINE) {
1478				Bookmark bookmark = conversation.getBookmark();
1479				if (bookmark != null) {
1480					bookmark.setNick(nick);
1481					pushBookmarks(bookmark.getAccount());
1482				}
1483				joinMuc(conversation);
1484			}
1485		}
1486	}
1487
1488	public void leaveMuc(Conversation conversation) {
1489		Account account = conversation.getAccount();
1490		account.pendingConferenceJoins.remove(conversation);
1491		account.pendingConferenceLeaves.remove(conversation);
1492		if (account.getStatus() == Account.State.ONLINE) {
1493			PresencePacket packet = new PresencePacket();
1494			packet.setTo(conversation.getJid());
1495			packet.setFrom(conversation.getAccount().getJid());
1496			packet.setAttribute("type", "unavailable");
1497			sendPresencePacket(conversation.getAccount(), packet);
1498			conversation.getMucOptions().setOffline();
1499			conversation.deregisterWithBookmark();
1500			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()
1501					+ ": leaving muc " + conversation.getJid());
1502		} else {
1503			account.pendingConferenceLeaves.add(conversation);
1504		}
1505	}
1506
1507	private String findConferenceServer(final Account account) {
1508		String server;
1509		if (account.getXmppConnection() != null) {
1510			server = account.getXmppConnection().getMucServer();
1511			if (server != null) {
1512				return server;
1513			}
1514		}
1515		for (Account other : getAccounts()) {
1516			if (other != account && other.getXmppConnection() != null) {
1517				server = other.getXmppConnection().getMucServer();
1518				if (server != null) {
1519					return server;
1520				}
1521			}
1522		}
1523		return null;
1524	}
1525
1526	public void createAdhocConference(final Account account, final Iterable<Jid> jids, final UiCallback<Conversation> callback) {
1527		Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString());
1528		if (account.getStatus() == Account.State.ONLINE) {
1529			try {
1530				String server = findConferenceServer(account);
1531				if (server == null) {
1532					if (callback != null) {
1533						callback.error(R.string.no_conference_server_found, null);
1534					}
1535					return;
1536				}
1537				String name = new BigInteger(75, getRNG()).toString(32);
1538				Jid jid = Jid.fromParts(name, server, null);
1539				final Conversation conversation = findOrCreateConversation(account, jid, true);
1540				joinMuc(conversation);
1541				Bundle options = new Bundle();
1542				options.putString("muc#roomconfig_persistentroom", "1");
1543				options.putString("muc#roomconfig_membersonly", "1");
1544				options.putString("muc#roomconfig_publicroom", "0");
1545				options.putString("muc#roomconfig_whois", "anyone");
1546				pushConferenceConfiguration(conversation, options, new OnConferenceOptionsPushed() {
1547					@Override
1548					public void onPushSucceeded() {
1549						for (Jid invite : jids) {
1550							invite(conversation, invite);
1551						}
1552						if (account.countPresences() > 1) {
1553							directInvite(conversation, account.getJid().toBareJid());
1554						}
1555						if (callback != null) {
1556							callback.success(conversation);
1557						}
1558					}
1559
1560					@Override
1561					public void onPushFailed() {
1562						if (callback != null) {
1563							callback.error(R.string.conference_creation_failed, conversation);
1564						}
1565					}
1566				});
1567
1568			} catch (InvalidJidException e) {
1569				if (callback != null) {
1570					callback.error(R.string.conference_creation_failed, null);
1571				}
1572			}
1573		} else {
1574			if (callback != null) {
1575				callback.error(R.string.not_connected_try_again, null);
1576			}
1577		}
1578	}
1579
1580	public void fetchConferenceConfiguration(final Conversation conversation) {
1581		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
1582		request.setTo(conversation.getJid().toBareJid());
1583		request.query("http://jabber.org/protocol/disco#info");
1584		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
1585			@Override
1586			public void onIqPacketReceived(Account account, IqPacket packet) {
1587				if (packet.getType() != IqPacket.TYPE.ERROR) {
1588					ArrayList<String> features = new ArrayList<>();
1589					for (Element child : packet.query().getChildren()) {
1590						if (child != null && child.getName().equals("feature")) {
1591							String var = child.getAttribute("var");
1592							if (var != null) {
1593								features.add(var);
1594							}
1595						}
1596					}
1597					conversation.getMucOptions().updateFeatures(features);
1598					updateConversationUi();
1599				}
1600			}
1601		});
1602	}
1603
1604	public void pushConferenceConfiguration(final Conversation conversation, final Bundle options, final OnConferenceOptionsPushed callback) {
1605		IqPacket request = new IqPacket(IqPacket.TYPE.GET);
1606		request.setTo(conversation.getJid().toBareJid());
1607		request.query("http://jabber.org/protocol/muc#owner");
1608		sendIqPacket(conversation.getAccount(), request, new OnIqPacketReceived() {
1609			@Override
1610			public void onIqPacketReceived(Account account, IqPacket packet) {
1611				if (packet.getType() != IqPacket.TYPE.ERROR) {
1612					Data data = Data.parse(packet.query().findChild("x", "jabber:x:data"));
1613					for (Field field : data.getFields()) {
1614						if (options.containsKey(field.getName())) {
1615							field.setValue(options.getString(field.getName()));
1616						}
1617					}
1618					data.submit();
1619					IqPacket set = new IqPacket(IqPacket.TYPE.SET);
1620					set.setTo(conversation.getJid().toBareJid());
1621					set.query("http://jabber.org/protocol/muc#owner").addChild(data);
1622					sendIqPacket(account, set, new OnIqPacketReceived() {
1623						@Override
1624						public void onIqPacketReceived(Account account, IqPacket packet) {
1625							if (packet.getType() == IqPacket.TYPE.RESULT) {
1626								if (callback != null) {
1627									callback.onPushSucceeded();
1628								}
1629							} else {
1630								if (callback != null) {
1631									callback.onPushFailed();
1632								}
1633							}
1634						}
1635					});
1636				} else {
1637					if (callback != null) {
1638						callback.onPushFailed();
1639					}
1640				}
1641			}
1642		});
1643	}
1644
1645	public void pushSubjectToConference(final Conversation conference, final String subject) {
1646		MessagePacket packet = this.getMessageGenerator().conferenceSubject(conference, subject);
1647		this.sendMessagePacket(conference.getAccount(), packet);
1648		final MucOptions mucOptions = conference.getMucOptions();
1649		final MucOptions.User self = mucOptions.getSelf();
1650		if (!mucOptions.persistent() && self.getAffiliation().ranks(MucOptions.Affiliation.OWNER)) {
1651			Bundle options = new Bundle();
1652			options.putString("muc#roomconfig_persistentroom", "1");
1653			this.pushConferenceConfiguration(conference, options, null);
1654		}
1655	}
1656
1657	public void changeAffiliationInConference(final Conversation conference, Jid user, MucOptions.Affiliation affiliation, final OnAffiliationChanged callback) {
1658		final Jid jid = user.toBareJid();
1659		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jid, affiliation.toString());
1660		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
1661			@Override
1662			public void onIqPacketReceived(Account account, IqPacket packet) {
1663				if (packet.getType() == IqPacket.TYPE.RESULT) {
1664					callback.onAffiliationChangedSuccessful(jid);
1665				} else {
1666					callback.onAffiliationChangeFailed(jid, R.string.could_not_change_affiliation);
1667				}
1668			}
1669		});
1670	}
1671
1672	public void changeAffiliationsInConference(final Conversation conference, MucOptions.Affiliation before, MucOptions.Affiliation after) {
1673		List<Jid> jids = new ArrayList<>();
1674		for (MucOptions.User user : conference.getMucOptions().getUsers()) {
1675			if (user.getAffiliation() == before) {
1676				jids.add(user.getJid());
1677			}
1678		}
1679		IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString());
1680		sendIqPacket(conference.getAccount(), request, null);
1681	}
1682
1683	public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) {
1684		IqPacket request = this.mIqGenerator.changeRole(conference, nick, role.toString());
1685		Log.d(Config.LOGTAG, request.toString());
1686		sendIqPacket(conference.getAccount(), request, new OnIqPacketReceived() {
1687			@Override
1688			public void onIqPacketReceived(Account account, IqPacket packet) {
1689				Log.d(Config.LOGTAG, packet.toString());
1690				if (packet.getType() == IqPacket.TYPE.RESULT) {
1691					callback.onRoleChangedSuccessful(nick);
1692				} else {
1693					callback.onRoleChangeFailed(nick, R.string.could_not_change_role);
1694				}
1695			}
1696		});
1697	}
1698
1699	public void disconnect(Account account, boolean force) {
1700		if ((account.getStatus() == Account.State.ONLINE)
1701				|| (account.getStatus() == Account.State.DISABLED)) {
1702			if (!force) {
1703				List<Conversation> conversations = getConversations();
1704				for (Conversation conversation : conversations) {
1705					if (conversation.getAccount() == account) {
1706						if (conversation.getMode() == Conversation.MODE_MULTI) {
1707							leaveMuc(conversation);
1708						} else {
1709							if (conversation.endOtrIfNeeded()) {
1710								Log.d(Config.LOGTAG, account.getJid().toBareJid()
1711										+ ": ended otr session with "
1712										+ conversation.getJid());
1713							}
1714						}
1715					}
1716				}
1717				sendOfflinePresence(account);
1718			}
1719			account.getXmppConnection().disconnect(force);
1720		}
1721	}
1722
1723	@Override
1724	public IBinder onBind(Intent intent) {
1725		return mBinder;
1726	}
1727
1728	public void updateMessage(Message message) {
1729		databaseBackend.updateMessage(message);
1730		updateConversationUi();
1731	}
1732
1733	protected void syncDirtyContacts(Account account) {
1734		for (Contact contact : account.getRoster().getContacts()) {
1735			if (contact.getOption(Contact.Options.DIRTY_PUSH)) {
1736				pushContactToServer(contact);
1737			}
1738			if (contact.getOption(Contact.Options.DIRTY_DELETE)) {
1739				deleteContactOnServer(contact);
1740			}
1741		}
1742	}
1743
1744	public void createContact(Contact contact) {
1745		SharedPreferences sharedPref = getPreferences();
1746		boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
1747		if (autoGrant) {
1748			contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
1749			contact.setOption(Contact.Options.ASKING);
1750		}
1751		pushContactToServer(contact);
1752	}
1753
1754	public void onOtrSessionEstablished(Conversation conversation) {
1755		final Account account = conversation.getAccount();
1756		final Session otrSession = conversation.getOtrSession();
1757		Log.d(Config.LOGTAG,
1758				account.getJid().toBareJid() + " otr session established with "
1759						+ conversation.getJid() + "/"
1760						+ otrSession.getSessionID().getUserID());
1761		conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
1762
1763			@Override
1764			public void onMessageFound(Message message) {
1765				SessionID id = otrSession.getSessionID();
1766				try {
1767					message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID()));
1768				} catch (InvalidJidException e) {
1769					return;
1770				}
1771				if (message.getType() == Message.TYPE_TEXT) {
1772					MessagePacket outPacket = mMessageGenerator.generateOtrChat(message, true);
1773					if (outPacket != null) {
1774						message.setStatus(Message.STATUS_SEND);
1775						databaseBackend.updateMessage(message);
1776						sendMessagePacket(account, outPacket);
1777					}
1778				} else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
1779					mJingleConnectionManager.createNewConnection(message);
1780				}
1781				updateConversationUi();
1782			}
1783		});
1784	}
1785
1786	public boolean renewSymmetricKey(Conversation conversation) {
1787		Account account = conversation.getAccount();
1788		byte[] symmetricKey = new byte[32];
1789		this.mRandom.nextBytes(symmetricKey);
1790		Session otrSession = conversation.getOtrSession();
1791		if (otrSession != null) {
1792			MessagePacket packet = new MessagePacket();
1793			packet.setType(MessagePacket.TYPE_CHAT);
1794			packet.setFrom(account.getJid());
1795			packet.addChild("private", "urn:xmpp:carbons:2");
1796			packet.addChild("no-copy", "urn:xmpp:hints");
1797			packet.setAttribute("to", otrSession.getSessionID().getAccountID() + "/"
1798					+ otrSession.getSessionID().getUserID());
1799			try {
1800				packet.setBody(otrSession
1801						.transformSending(CryptoHelper.FILETRANSFER
1802								+ CryptoHelper.bytesToHex(symmetricKey))[0]);
1803				sendMessagePacket(account, packet);
1804				conversation.setSymmetricKey(symmetricKey);
1805				return true;
1806			} catch (OtrException e) {
1807				return false;
1808			}
1809		}
1810		return false;
1811	}
1812
1813	public void pushContactToServer(final Contact contact) {
1814		contact.resetOption(Contact.Options.DIRTY_DELETE);
1815		contact.setOption(Contact.Options.DIRTY_PUSH);
1816		final Account account = contact.getAccount();
1817		if (account.getStatus() == Account.State.ONLINE) {
1818			final boolean ask = contact.getOption(Contact.Options.ASKING);
1819			final boolean sendUpdates = contact
1820					.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)
1821					&& contact.getOption(Contact.Options.PREEMPTIVE_GRANT);
1822			final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
1823			iq.query(Xmlns.ROSTER).addChild(contact.asElement());
1824			account.getXmppConnection().sendIqPacket(iq, null);
1825			if (sendUpdates) {
1826				sendPresencePacket(account,
1827						mPresenceGenerator.sendPresenceUpdatesTo(contact));
1828			}
1829			if (ask) {
1830				sendPresencePacket(account,
1831						mPresenceGenerator.requestPresenceUpdatesFrom(contact));
1832			}
1833		}
1834	}
1835
1836	public void publishAvatar(final Account account,
1837							  final Uri image,
1838							  final UiCallback<Avatar> callback) {
1839		final Bitmap.CompressFormat format = Config.AVATAR_FORMAT;
1840		final int size = Config.AVATAR_SIZE;
1841		final Avatar avatar = getFileBackend()
1842				.getPepAvatar(image, size, format);
1843		if (avatar != null) {
1844			avatar.height = size;
1845			avatar.width = size;
1846			if (format.equals(Bitmap.CompressFormat.WEBP)) {
1847				avatar.type = "image/webp";
1848			} else if (format.equals(Bitmap.CompressFormat.JPEG)) {
1849				avatar.type = "image/jpeg";
1850			} else if (format.equals(Bitmap.CompressFormat.PNG)) {
1851				avatar.type = "image/png";
1852			}
1853			if (!getFileBackend().save(avatar)) {
1854				callback.error(R.string.error_saving_avatar, avatar);
1855				return;
1856			}
1857			final IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
1858			this.sendIqPacket(account, packet, new OnIqPacketReceived() {
1859
1860				@Override
1861				public void onIqPacketReceived(Account account, IqPacket result) {
1862					if (result.getType() == IqPacket.TYPE.RESULT) {
1863						final IqPacket packet = XmppConnectionService.this.mIqGenerator
1864								.publishAvatarMetadata(avatar);
1865						sendIqPacket(account, packet, new OnIqPacketReceived() {
1866
1867							@Override
1868							public void onIqPacketReceived(Account account,
1869														   IqPacket result) {
1870								if (result.getType() == IqPacket.TYPE.RESULT) {
1871									if (account.setAvatar(avatar.getFilename())) {
1872										databaseBackend.updateAccount(account);
1873									}
1874									callback.success(avatar);
1875								} else {
1876									callback.error(
1877											R.string.error_publish_avatar_server_reject,
1878											avatar);
1879								}
1880							}
1881						});
1882					} else {
1883						callback.error(
1884								R.string.error_publish_avatar_server_reject,
1885								avatar);
1886					}
1887				}
1888			});
1889		} else {
1890			callback.error(R.string.error_publish_avatar_converting, null);
1891		}
1892	}
1893
1894	public void fetchAvatar(Account account, Avatar avatar) {
1895		fetchAvatar(account, avatar, null);
1896	}
1897
1898	private static String generateFetchKey(Account account, final Avatar avatar) {
1899		return account.getJid().toBareJid()+"_"+avatar.owner+"_"+avatar.sha1sum;
1900	}
1901
1902	public void fetchAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
1903		final String KEY = generateFetchKey(account, avatar);
1904		synchronized(this.mInProgressAvatarFetches) {
1905			if (this.mInProgressAvatarFetches.contains(KEY)) {
1906				return;
1907			} else {
1908				switch (avatar.origin) {
1909					case PEP:
1910						this.mInProgressAvatarFetches.add(KEY);
1911						fetchAvatarPep(account, avatar, callback);
1912						break;
1913					case VCARD:
1914						this.mInProgressAvatarFetches.add(KEY);
1915						fetchAvatarVcard(account, avatar, callback);
1916						break;
1917				}
1918			}
1919		}
1920	}
1921
1922	private void fetchAvatarPep(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
1923		IqPacket packet = this.mIqGenerator.retrievePepAvatar(avatar);
1924		sendIqPacket(account, packet, new OnIqPacketReceived() {
1925
1926			@Override
1927			public void onIqPacketReceived(Account account, IqPacket result) {
1928				synchronized (mInProgressAvatarFetches) {
1929					mInProgressAvatarFetches.remove(generateFetchKey(account, avatar));
1930				}
1931				final String ERROR = account.getJid().toBareJid()
1932						+ ": fetching avatar for " + avatar.owner + " failed ";
1933				if (result.getType() == IqPacket.TYPE.RESULT) {
1934					avatar.image = mIqParser.avatarData(result);
1935					if (avatar.image != null) {
1936						if (getFileBackend().save(avatar)) {
1937							if (account.getJid().toBareJid().equals(avatar.owner)) {
1938								if (account.setAvatar(avatar.getFilename())) {
1939									databaseBackend.updateAccount(account);
1940								}
1941								getAvatarService().clear(account);
1942								updateConversationUi();
1943								updateAccountUi();
1944							} else {
1945								Contact contact = account.getRoster()
1946										.getContact(avatar.owner);
1947								contact.setAvatar(avatar);
1948								getAvatarService().clear(contact);
1949								updateConversationUi();
1950								updateRosterUi();
1951							}
1952							if (callback != null) {
1953								callback.success(avatar);
1954							}
1955							Log.d(Config.LOGTAG, account.getJid().toBareJid()
1956									+ ": succesfuly fetched pep avatar for " + avatar.owner);
1957							return;
1958						}
1959					} else {
1960
1961						Log.d(Config.LOGTAG, ERROR + "(parsing error)");
1962					}
1963				} else {
1964					Element error = result.findChild("error");
1965					if (error == null) {
1966						Log.d(Config.LOGTAG, ERROR + "(server error)");
1967					} else {
1968						Log.d(Config.LOGTAG, ERROR + error.toString());
1969					}
1970				}
1971				if (callback != null) {
1972					callback.error(0, null);
1973				}
1974
1975			}
1976		});
1977	}
1978
1979	private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
1980		IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar);
1981		this.sendIqPacket(account,packet,new OnIqPacketReceived() {
1982			@Override
1983			public void onIqPacketReceived(Account account, IqPacket packet) {
1984				synchronized(mInProgressAvatarFetches) {
1985					mInProgressAvatarFetches.remove(generateFetchKey(account,avatar));
1986				}
1987				if (packet.getType() == IqPacket.TYPE.RESULT) {
1988					Element vCard = packet.findChild("vCard","vcard-temp");
1989					Element photo = vCard != null ? vCard.findChild("PHOTO") : null;
1990					Element binval = photo != null ? photo.findChild("BINVAL") : null;
1991					String image = binval != null ? binval.getContent() : null;
1992					if (image != null) {
1993						avatar.image = image;
1994						if (getFileBackend().save(avatar)) {
1995							Log.d(Config.LOGTAG, account.getJid().toBareJid()
1996									+ ": successfully fetched vCard avatar for " + avatar.owner);
1997							Contact contact = account.getRoster()
1998									.getContact(avatar.owner);
1999							contact.setAvatar(avatar);
2000							getAvatarService().clear(contact);
2001							updateConversationUi();
2002							updateRosterUi();
2003						}
2004					}
2005				}
2006			}
2007		});
2008	}
2009
2010	public void checkForAvatar(Account account, final UiCallback<Avatar> callback) {
2011		IqPacket packet = this.mIqGenerator.retrieveAvatarMetaData(null);
2012		this.sendIqPacket(account, packet, new OnIqPacketReceived() {
2013
2014			@Override
2015			public void onIqPacketReceived(Account account, IqPacket packet) {
2016				if (packet.getType() == IqPacket.TYPE.RESULT) {
2017					Element pubsub = packet.findChild("pubsub",
2018							"http://jabber.org/protocol/pubsub");
2019					if (pubsub != null) {
2020						Element items = pubsub.findChild("items");
2021						if (items != null) {
2022							Avatar avatar = Avatar.parseMetadata(items);
2023							if (avatar != null) {
2024								avatar.owner = account.getJid().toBareJid();
2025								if (fileBackend.isAvatarCached(avatar)) {
2026									if (account.setAvatar(avatar.getFilename())) {
2027										databaseBackend.updateAccount(account);
2028									}
2029									getAvatarService().clear(account);
2030									callback.success(avatar);
2031								} else {
2032									fetchAvatarPep(account, avatar, callback);
2033								}
2034								return;
2035							}
2036						}
2037					}
2038				}
2039				callback.error(0, null);
2040			}
2041		});
2042	}
2043
2044	public void deleteContactOnServer(Contact contact) {
2045		contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
2046		contact.resetOption(Contact.Options.DIRTY_PUSH);
2047		contact.setOption(Contact.Options.DIRTY_DELETE);
2048		Account account = contact.getAccount();
2049		if (account.getStatus() == Account.State.ONLINE) {
2050			IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
2051			Element item = iq.query(Xmlns.ROSTER).addChild("item");
2052			item.setAttribute("jid", contact.getJid().toString());
2053			item.setAttribute("subscription", "remove");
2054			account.getXmppConnection().sendIqPacket(iq, null);
2055		}
2056	}
2057
2058	public void updateConversation(Conversation conversation) {
2059		this.databaseBackend.updateConversation(conversation);
2060	}
2061
2062	public void reconnectAccount(final Account account, final boolean force) {
2063		synchronized (account) {
2064			if (account.getXmppConnection() != null) {
2065				disconnect(account, force);
2066			}
2067			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
2068
2069				synchronized (this.mInProgressAvatarFetches) {
2070					for(Iterator<String> iterator = this.mInProgressAvatarFetches.iterator(); iterator.hasNext();) {
2071						final String KEY = iterator.next();
2072						if (KEY.startsWith(account.getJid().toBareJid()+"_")) {
2073							iterator.remove();
2074						}
2075					}
2076				}
2077
2078				if (account.getXmppConnection() == null) {
2079					account.setXmppConnection(createConnection(account));
2080				}
2081				Thread thread = new Thread(account.getXmppConnection());
2082				thread.start();
2083				scheduleWakeUpCall(Config.CONNECT_TIMEOUT, account.getUuid().hashCode());
2084			} else {
2085				account.getRoster().clearPresences();
2086				account.setXmppConnection(null);
2087			}
2088		}
2089	}
2090
2091	public void reconnectAccountInBackground(final Account account) {
2092		new Thread(new Runnable() {
2093			@Override
2094			public void run() {
2095				reconnectAccount(account,false);
2096			}
2097		}).start();
2098	}
2099
2100	public void invite(Conversation conversation, Jid contact) {
2101		MessagePacket packet = mMessageGenerator.invite(conversation, contact);
2102		sendMessagePacket(conversation.getAccount(), packet);
2103	}
2104
2105	public void directInvite(Conversation conversation, Jid jid) {
2106		MessagePacket packet = mMessageGenerator.directInvite(conversation,jid);
2107		sendMessagePacket(conversation.getAccount(),packet);
2108	}
2109
2110	public void resetSendingToWaiting(Account account) {
2111		for (Conversation conversation : getConversations()) {
2112			if (conversation.getAccount() == account) {
2113				conversation.findUnsentTextMessages(new Conversation.OnMessageFound() {
2114
2115					@Override
2116					public void onMessageFound(Message message) {
2117						markMessage(message, Message.STATUS_WAITING);
2118					}
2119				});
2120			}
2121		}
2122	}
2123
2124	public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) {
2125		if (uuid == null) {
2126			return null;
2127		}
2128		for (Conversation conversation : getConversations()) {
2129			if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) {
2130				final Message message = conversation.findSentMessageWithUuid(uuid);
2131				if (message != null) {
2132					markMessage(message, status);
2133				}
2134				return message;
2135			}
2136		}
2137		return null;
2138	}
2139
2140	public boolean markMessage(Conversation conversation, String uuid,
2141							   int status) {
2142		if (uuid == null) {
2143			return false;
2144		} else {
2145			Message message = conversation.findSentMessageWithUuid(uuid);
2146			if (message != null) {
2147				markMessage(message, status);
2148				return true;
2149			} else {
2150				return false;
2151			}
2152		}
2153	}
2154
2155	public void markMessage(Message message, int status) {
2156		if (status == Message.STATUS_SEND_FAILED
2157				&& (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
2158				.getStatus() == Message.STATUS_SEND_DISPLAYED)) {
2159			return;
2160		}
2161		message.setStatus(status);
2162		databaseBackend.updateMessage(message);
2163		updateConversationUi();
2164	}
2165
2166	public SharedPreferences getPreferences() {
2167		return PreferenceManager
2168				.getDefaultSharedPreferences(getApplicationContext());
2169	}
2170
2171	public boolean forceEncryption() {
2172		return getPreferences().getBoolean("force_encryption", false);
2173	}
2174
2175	public boolean confirmMessages() {
2176		return getPreferences().getBoolean("confirm_messages", true);
2177	}
2178
2179	public boolean sendChatStates() {
2180		return getPreferences().getBoolean("chat_states", false);
2181	}
2182
2183	public boolean saveEncryptedMessages() {
2184		return !getPreferences().getBoolean("dont_save_encrypted", false);
2185	}
2186
2187	public boolean indicateReceived() {
2188		return getPreferences().getBoolean("indicate_received", false);
2189	}
2190
2191	public int unreadCount() {
2192		int count = 0;
2193		for(Conversation conversation : getConversations()) {
2194			count += conversation.unreadCount();
2195		}
2196		return count;
2197	}
2198
2199	public void updateConversationUi() {
2200		if (mOnConversationUpdate != null) {
2201			mOnConversationUpdate.onConversationUpdate();
2202		}
2203	}
2204
2205	public void updateAccountUi() {
2206		if (mOnAccountUpdate != null) {
2207			mOnAccountUpdate.onAccountUpdate();
2208		}
2209	}
2210
2211	public void updateRosterUi() {
2212		if (mOnRosterUpdate != null) {
2213			mOnRosterUpdate.onRosterUpdate();
2214		}
2215	}
2216
2217	public void updateBlocklistUi(final OnUpdateBlocklist.Status status) {
2218		if (mOnUpdateBlocklist != null) {
2219			mOnUpdateBlocklist.OnUpdateBlocklist(status);
2220		}
2221	}
2222
2223	public void updateMucRosterUi() {
2224		if (mOnMucRosterUpdate != null) {
2225			mOnMucRosterUpdate.onMucRosterUpdate();
2226		}
2227	}
2228
2229	public Account findAccountByJid(final Jid accountJid) {
2230		for (Account account : this.accounts) {
2231			if (account.getJid().toBareJid().equals(accountJid.toBareJid())) {
2232				return account;
2233			}
2234		}
2235		return null;
2236	}
2237
2238	public Conversation findConversationByUuid(String uuid) {
2239		for (Conversation conversation : getConversations()) {
2240			if (conversation.getUuid().equals(uuid)) {
2241				return conversation;
2242			}
2243		}
2244		return null;
2245	}
2246
2247	public void markRead(final Conversation conversation) {
2248		mNotificationService.clear(conversation);
2249		conversation.markRead();
2250	}
2251
2252	public void sendReadMarker(final Conversation conversation) {
2253		final Message markable = conversation.getLatestMarkableMessage();
2254		this.markRead(conversation);
2255		if (confirmMessages() && markable != null && markable.getRemoteMsgId() != null) {
2256			Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString());
2257			Account account = conversation.getAccount();
2258			final Jid to = markable.getCounterpart();
2259			MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId());
2260			this.sendMessagePacket(conversation.getAccount(), packet);
2261		}
2262		updateConversationUi();
2263	}
2264
2265	public SecureRandom getRNG() {
2266		return this.mRandom;
2267	}
2268
2269	public MemorizingTrustManager getMemorizingTrustManager() {
2270		return this.mMemorizingTrustManager;
2271	}
2272
2273	public void setMemorizingTrustManager(MemorizingTrustManager trustManager) {
2274		this.mMemorizingTrustManager = trustManager;
2275	}
2276
2277	public void updateMemorizingTrustmanager() {
2278		final MemorizingTrustManager tm;
2279		final boolean dontTrustSystemCAs = getPreferences().getBoolean("dont_trust_system_cas", false);
2280		if (dontTrustSystemCAs) {
2281			 tm = new MemorizingTrustManager(getApplicationContext(), null);
2282		} else {
2283			tm = new MemorizingTrustManager(getApplicationContext());
2284		}
2285		setMemorizingTrustManager(tm);
2286	}
2287
2288	public PowerManager getPowerManager() {
2289		return this.pm;
2290	}
2291
2292	public LruCache<String, Bitmap> getBitmapCache() {
2293		return this.mBitmapCache;
2294	}
2295
2296	public void syncRosterToDisk(final Account account) {
2297		new Thread(new Runnable() {
2298
2299			@Override
2300			public void run() {
2301				databaseBackend.writeRoster(account.getRoster());
2302			}
2303		}).start();
2304
2305	}
2306
2307	public List<String> getKnownHosts() {
2308		final List<String> hosts = new ArrayList<>();
2309		for (final Account account : getAccounts()) {
2310			if (!hosts.contains(account.getServer().toString())) {
2311				hosts.add(account.getServer().toString());
2312			}
2313			for (final Contact contact : account.getRoster().getContacts()) {
2314				if (contact.showInRoster()) {
2315					final String server = contact.getServer().toString();
2316					if (server != null && !hosts.contains(server)) {
2317						hosts.add(server);
2318					}
2319				}
2320			}
2321		}
2322		return hosts;
2323	}
2324
2325	public List<String> getKnownConferenceHosts() {
2326		final ArrayList<String> mucServers = new ArrayList<>();
2327		for (final Account account : accounts) {
2328			if (account.getXmppConnection() != null) {
2329				final String server = account.getXmppConnection().getMucServer();
2330				if (server != null && !mucServers.contains(server)) {
2331					mucServers.add(server);
2332				}
2333			}
2334		}
2335		return mucServers;
2336	}
2337
2338	public void sendMessagePacket(Account account, MessagePacket packet) {
2339		XmppConnection connection = account.getXmppConnection();
2340		if (connection != null) {
2341			connection.sendMessagePacket(packet);
2342		}
2343	}
2344
2345	public void sendPresencePacket(Account account, PresencePacket packet) {
2346		XmppConnection connection = account.getXmppConnection();
2347		if (connection != null) {
2348			connection.sendPresencePacket(packet);
2349		}
2350	}
2351
2352	public void sendIqPacket(final Account account, final IqPacket packet, final OnIqPacketReceived callback) {
2353		final XmppConnection connection = account.getXmppConnection();
2354		if (connection != null) {
2355			connection.sendIqPacket(packet, callback);
2356		}
2357	}
2358
2359	public void sendPresence(final Account account) {
2360		sendPresencePacket(account, mPresenceGenerator.sendPresence(account));
2361	}
2362
2363	public void sendOfflinePresence(final Account account) {
2364		sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account));
2365	}
2366
2367	public MessageGenerator getMessageGenerator() {
2368		return this.mMessageGenerator;
2369	}
2370
2371	public PresenceGenerator getPresenceGenerator() {
2372		return this.mPresenceGenerator;
2373	}
2374
2375	public IqGenerator getIqGenerator() {
2376		return this.mIqGenerator;
2377	}
2378
2379	public IqParser getIqParser() {
2380		return this.mIqParser;
2381	}
2382
2383	public JingleConnectionManager getJingleConnectionManager() {
2384		return this.mJingleConnectionManager;
2385	}
2386
2387	public MessageArchiveService getMessageArchiveService() {
2388		return this.mMessageArchiveService;
2389	}
2390
2391	public List<Contact> findContacts(Jid jid) {
2392		ArrayList<Contact> contacts = new ArrayList<>();
2393		for (Account account : getAccounts()) {
2394			if (!account.isOptionSet(Account.OPTION_DISABLED)) {
2395				Contact contact = account.getRoster().getContactFromRoster(jid);
2396				if (contact != null) {
2397					contacts.add(contact);
2398				}
2399			}
2400		}
2401		return contacts;
2402	}
2403
2404	public NotificationService getNotificationService() {
2405		return this.mNotificationService;
2406	}
2407
2408	public HttpConnectionManager getHttpConnectionManager() {
2409		return this.mHttpConnectionManager;
2410	}
2411
2412	public void resendFailedMessages(final Message message) {
2413		final Collection<Message> messages = new ArrayList<>();
2414		Message current = message;
2415		while (current.getStatus() == Message.STATUS_SEND_FAILED) {
2416			messages.add(current);
2417			if (current.mergeable(current.next())) {
2418				current = current.next();
2419			} else {
2420				break;
2421			}
2422		}
2423		for (final Message msg : messages) {
2424			markMessage(msg, Message.STATUS_WAITING);
2425			this.resendMessage(msg);
2426		}
2427	}
2428
2429	public void clearConversationHistory(final Conversation conversation) {
2430		conversation.clearMessages();
2431		conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam
2432		new Thread(new Runnable() {
2433			@Override
2434			public void run() {
2435				databaseBackend.deleteMessagesInConversation(conversation);
2436			}
2437		}).start();
2438	}
2439
2440	public void sendBlockRequest(final Blockable blockable) {
2441		if (blockable != null && blockable.getBlockedJid() != null) {
2442			final Jid jid = blockable.getBlockedJid();
2443			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid), new OnIqPacketReceived() {
2444
2445				@Override
2446				public void onIqPacketReceived(final Account account, final IqPacket packet) {
2447					if (packet.getType() == IqPacket.TYPE.RESULT) {
2448						account.getBlocklist().add(jid);
2449						updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
2450					}
2451				}
2452			});
2453		}
2454	}
2455
2456	public void sendUnblockRequest(final Blockable blockable) {
2457		if (blockable != null && blockable.getJid() != null) {
2458			final Jid jid = blockable.getBlockedJid();
2459			this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() {
2460				@Override
2461				public void onIqPacketReceived(final Account account, final IqPacket packet) {
2462					if (packet.getType() == IqPacket.TYPE.RESULT) {
2463						account.getBlocklist().remove(jid);
2464						updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
2465					}
2466				}
2467			});
2468		}
2469	}
2470
2471	public interface OnMoreMessagesLoaded {
2472		public void onMoreMessagesLoaded(int count, Conversation conversation);
2473
2474		public void informUser(int r);
2475	}
2476
2477	public interface OnAccountPasswordChanged {
2478		public void onPasswordChangeSucceeded();
2479
2480		public void onPasswordChangeFailed();
2481	}
2482
2483	public interface OnAffiliationChanged {
2484		public void onAffiliationChangedSuccessful(Jid jid);
2485
2486		public void onAffiliationChangeFailed(Jid jid, int resId);
2487	}
2488
2489	public interface OnRoleChanged {
2490		public void onRoleChangedSuccessful(String nick);
2491
2492		public void onRoleChangeFailed(String nick, int resid);
2493	}
2494
2495	public interface OnConversationUpdate {
2496		public void onConversationUpdate();
2497	}
2498
2499	public interface OnAccountUpdate {
2500		public void onAccountUpdate();
2501	}
2502
2503	public interface OnRosterUpdate {
2504		public void onRosterUpdate();
2505	}
2506
2507	public interface OnMucRosterUpdate {
2508		public void onMucRosterUpdate();
2509	}
2510
2511	public interface OnConferenceOptionsPushed {
2512		public void onPushSucceeded();
2513
2514		public void onPushFailed();
2515	}
2516
2517	public class XmppConnectionBinder extends Binder {
2518		public XmppConnectionService getService() {
2519			return XmppConnectionService.this;
2520		}
2521	}
2522}