XmppActivity.java

   1package eu.siacs.conversations.ui;
   2
   3import android.Manifest;
   4import android.annotation.SuppressLint;
   5import android.annotation.TargetApi;
   6import android.app.ActionBar;
   7import android.app.Activity;
   8import android.app.AlertDialog;
   9import android.app.AlertDialog.Builder;
  10import android.app.PendingIntent;
  11import android.content.ActivityNotFoundException;
  12import android.content.ClipData;
  13import android.content.ClipboardManager;
  14import android.content.ComponentName;
  15import android.content.Context;
  16import android.content.DialogInterface;
  17import android.content.DialogInterface.OnClickListener;
  18import android.content.Intent;
  19import android.content.IntentSender.SendIntentException;
  20import android.content.ServiceConnection;
  21import android.content.SharedPreferences;
  22import android.content.pm.PackageManager;
  23import android.content.pm.ResolveInfo;
  24import android.content.res.Resources;
  25import android.content.res.TypedArray;
  26import android.graphics.Bitmap;
  27import android.graphics.Color;
  28import android.graphics.Point;
  29import android.graphics.drawable.BitmapDrawable;
  30import android.graphics.drawable.Drawable;
  31import android.net.Uri;
  32import android.nfc.NdefMessage;
  33import android.nfc.NdefRecord;
  34import android.nfc.NfcAdapter;
  35import android.nfc.NfcEvent;
  36import android.os.AsyncTask;
  37import android.os.Build;
  38import android.os.Bundle;
  39import android.os.Handler;
  40import android.os.IBinder;
  41import android.os.PowerManager;
  42import android.os.SystemClock;
  43import android.preference.PreferenceManager;
  44import android.text.InputType;
  45import android.util.DisplayMetrics;
  46import android.util.Log;
  47import android.view.MenuItem;
  48import android.view.View;
  49import android.view.inputmethod.InputMethodManager;
  50import android.widget.CompoundButton;
  51import android.widget.EditText;
  52import android.widget.ImageView;
  53import android.widget.LinearLayout;
  54import android.widget.TextView;
  55import android.widget.Toast;
  56
  57import com.google.zxing.BarcodeFormat;
  58import com.google.zxing.EncodeHintType;
  59import com.google.zxing.WriterException;
  60import com.google.zxing.common.BitMatrix;
  61import com.google.zxing.qrcode.QRCodeWriter;
  62import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
  63
  64import net.java.otr4j.session.SessionID;
  65
  66import java.io.FileNotFoundException;
  67import java.lang.ref.WeakReference;
  68import java.util.ArrayList;
  69import java.util.Hashtable;
  70import java.util.List;
  71import java.util.concurrent.RejectedExecutionException;
  72
  73import eu.siacs.conversations.Config;
  74import eu.siacs.conversations.R;
  75import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
  76import eu.siacs.conversations.entities.Account;
  77import eu.siacs.conversations.entities.Contact;
  78import eu.siacs.conversations.entities.Conversation;
  79import eu.siacs.conversations.entities.Message;
  80import eu.siacs.conversations.entities.MucOptions;
  81import eu.siacs.conversations.entities.Presences;
  82import eu.siacs.conversations.services.AvatarService;
  83import eu.siacs.conversations.services.XmppConnectionService;
  84import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
  85import eu.siacs.conversations.ui.widget.Switch;
  86import eu.siacs.conversations.utils.CryptoHelper;
  87import eu.siacs.conversations.utils.ExceptionHelper;
  88import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
  89import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
  90import eu.siacs.conversations.xmpp.jid.InvalidJidException;
  91import eu.siacs.conversations.xmpp.jid.Jid;
  92
  93public abstract class XmppActivity extends Activity {
  94
  95	protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
  96	protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
  97	protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103;
  98	protected static final int REQUEST_BATTERY_OP = 0x13849ff;
  99
 100	public static final String EXTRA_ACCOUNT = "account";
 101
 102	public XmppConnectionService xmppConnectionService;
 103	public boolean xmppConnectionServiceBound = false;
 104	protected boolean registeredListeners = false;
 105
 106	protected int mPrimaryTextColor;
 107	protected int mSecondaryTextColor;
 108	protected int mTertiaryTextColor;
 109	protected int mPrimaryBackgroundColor;
 110	protected int mSecondaryBackgroundColor;
 111	protected int mColorRed;
 112	protected int mColorOrange;
 113	protected int mColorGreen;
 114	protected int mPrimaryColor;
 115
 116	protected boolean mUseSubject = true;
 117
 118	private DisplayMetrics metrics;
 119	protected int mTheme;
 120	protected boolean mUsingEnterKey = false;
 121
 122	protected Toast mToast;
 123
 124	protected void hideToast() {
 125		if (mToast != null) {
 126			mToast.cancel();
 127		}
 128	}
 129
 130	protected void replaceToast(String msg) {
 131		replaceToast(msg, true);
 132	}
 133
 134	protected void replaceToast(String msg, boolean showlong) {
 135		hideToast();
 136		mToast = Toast.makeText(this, msg ,showlong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT);
 137		mToast.show();
 138	}
 139
 140	protected Runnable onOpenPGPKeyPublished = new Runnable() {
 141		@Override
 142		public void run() {
 143			Toast.makeText(XmppActivity.this,R.string.openpgp_has_been_published, Toast.LENGTH_SHORT).show();
 144		}
 145	};
 146
 147	private long mLastUiRefresh = 0;
 148	private Handler mRefreshUiHandler = new Handler();
 149	private Runnable mRefreshUiRunnable = new Runnable() {
 150		@Override
 151		public void run() {
 152			mLastUiRefresh = SystemClock.elapsedRealtime();
 153			refreshUiReal();
 154		}
 155	};
 156
 157	protected ConferenceInvite mPendingConferenceInvite = null;
 158
 159
 160	protected final void refreshUi() {
 161		final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh;
 162		if (diff > Config.REFRESH_UI_INTERVAL) {
 163			mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
 164			runOnUiThread(mRefreshUiRunnable);
 165		} else {
 166			final long next = Config.REFRESH_UI_INTERVAL - diff;
 167			mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
 168			mRefreshUiHandler.postDelayed(mRefreshUiRunnable,next);
 169		}
 170	}
 171
 172	abstract protected void refreshUiReal();
 173
 174	protected interface OnValueEdited {
 175		public void onValueEdited(String value);
 176	}
 177
 178	public interface OnPresenceSelected {
 179		public void onPresenceSelected();
 180	}
 181
 182	protected ServiceConnection mConnection = new ServiceConnection() {
 183
 184		@Override
 185		public void onServiceConnected(ComponentName className, IBinder service) {
 186			XmppConnectionBinder binder = (XmppConnectionBinder) service;
 187			xmppConnectionService = binder.getService();
 188			xmppConnectionServiceBound = true;
 189			if (!registeredListeners && shouldRegisterListeners()) {
 190				registerListeners();
 191				registeredListeners = true;
 192			}
 193			onBackendConnected();
 194		}
 195
 196		@Override
 197		public void onServiceDisconnected(ComponentName arg0) {
 198			xmppConnectionServiceBound = false;
 199		}
 200	};
 201
 202	@Override
 203	protected void onStart() {
 204		super.onStart();
 205		if (!xmppConnectionServiceBound) {
 206			connectToBackend();
 207		} else {
 208			if (!registeredListeners) {
 209				this.registerListeners();
 210				this.registeredListeners = true;
 211			}
 212			this.onBackendConnected();
 213		}
 214	}
 215
 216	@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
 217	protected boolean shouldRegisterListeners() {
 218		if  (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
 219			return !isDestroyed() && !isFinishing();
 220		} else {
 221			return !isFinishing();
 222		}
 223	}
 224
 225	public void connectToBackend() {
 226		Intent intent = new Intent(this, XmppConnectionService.class);
 227		intent.setAction("ui");
 228		startService(intent);
 229		bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
 230	}
 231
 232	@Override
 233	protected void onStop() {
 234		super.onStop();
 235		if (xmppConnectionServiceBound) {
 236			if (registeredListeners) {
 237				this.unregisterListeners();
 238				this.registeredListeners = false;
 239			}
 240			unbindService(mConnection);
 241			xmppConnectionServiceBound = false;
 242		}
 243	}
 244
 245	protected void hideKeyboard() {
 246		InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
 247
 248		View focus = getCurrentFocus();
 249
 250		if (focus != null) {
 251
 252			inputManager.hideSoftInputFromWindow(focus.getWindowToken(),
 253					InputMethodManager.HIDE_NOT_ALWAYS);
 254		}
 255	}
 256
 257	public boolean hasPgp() {
 258		return xmppConnectionService.getPgpEngine() != null;
 259	}
 260
 261	public void showInstallPgpDialog() {
 262		Builder builder = new AlertDialog.Builder(this);
 263		builder.setTitle(getString(R.string.openkeychain_required));
 264		builder.setIconAttribute(android.R.attr.alertDialogIcon);
 265		builder.setMessage(getText(R.string.openkeychain_required_long));
 266		builder.setNegativeButton(getString(R.string.cancel), null);
 267		builder.setNeutralButton(getString(R.string.restart),
 268				new OnClickListener() {
 269
 270					@Override
 271					public void onClick(DialogInterface dialog, int which) {
 272						if (xmppConnectionServiceBound) {
 273							unbindService(mConnection);
 274							xmppConnectionServiceBound = false;
 275						}
 276						stopService(new Intent(XmppActivity.this,
 277									XmppConnectionService.class));
 278						finish();
 279					}
 280				});
 281		builder.setPositiveButton(getString(R.string.install),
 282				new OnClickListener() {
 283
 284					@Override
 285					public void onClick(DialogInterface dialog, int which) {
 286						Uri uri = Uri
 287							.parse("market://details?id=org.sufficientlysecure.keychain");
 288						Intent marketIntent = new Intent(Intent.ACTION_VIEW,
 289								uri);
 290						PackageManager manager = getApplicationContext()
 291							.getPackageManager();
 292						List<ResolveInfo> infos = manager
 293							.queryIntentActivities(marketIntent, 0);
 294						if (infos.size() > 0) {
 295							startActivity(marketIntent);
 296						} else {
 297							uri = Uri.parse("http://www.openkeychain.org/");
 298							Intent browserIntent = new Intent(
 299									Intent.ACTION_VIEW, uri);
 300							startActivity(browserIntent);
 301						}
 302						finish();
 303					}
 304				});
 305		builder.create().show();
 306	}
 307
 308	abstract void onBackendConnected();
 309
 310	protected void registerListeners() {
 311		if (this instanceof XmppConnectionService.OnConversationUpdate) {
 312			this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this);
 313		}
 314		if (this instanceof XmppConnectionService.OnAccountUpdate) {
 315			this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
 316		}
 317		if (this instanceof XmppConnectionService.OnCaptchaRequested) {
 318			this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
 319		}
 320		if (this instanceof XmppConnectionService.OnRosterUpdate) {
 321			this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
 322		}
 323		if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
 324			this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this);
 325		}
 326		if (this instanceof OnUpdateBlocklist) {
 327			this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this);
 328		}
 329		if (this instanceof XmppConnectionService.OnShowErrorToast) {
 330			this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
 331		}
 332		if (this instanceof OnKeyStatusUpdated) {
 333			this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this);
 334		}
 335	}
 336
 337	protected void unregisterListeners() {
 338		if (this instanceof XmppConnectionService.OnConversationUpdate) {
 339			this.xmppConnectionService.removeOnConversationListChangedListener();
 340		}
 341		if (this instanceof XmppConnectionService.OnAccountUpdate) {
 342			this.xmppConnectionService.removeOnAccountListChangedListener();
 343		}
 344		if (this instanceof XmppConnectionService.OnCaptchaRequested) {
 345			this.xmppConnectionService.removeOnCaptchaRequestedListener();
 346		}
 347		if (this instanceof XmppConnectionService.OnRosterUpdate) {
 348			this.xmppConnectionService.removeOnRosterUpdateListener();
 349		}
 350		if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
 351			this.xmppConnectionService.removeOnMucRosterUpdateListener();
 352		}
 353		if (this instanceof OnUpdateBlocklist) {
 354			this.xmppConnectionService.removeOnUpdateBlocklistListener();
 355		}
 356		if (this instanceof XmppConnectionService.OnShowErrorToast) {
 357			this.xmppConnectionService.removeOnShowErrorToastListener();
 358		}
 359		if (this instanceof OnKeyStatusUpdated) {
 360			this.xmppConnectionService.removeOnNewKeysAvailableListener();
 361		}
 362	}
 363
 364	@Override
 365	public boolean onOptionsItemSelected(final MenuItem item) {
 366		switch (item.getItemId()) {
 367			case R.id.action_settings:
 368				startActivity(new Intent(this, SettingsActivity.class));
 369				break;
 370			case R.id.action_accounts:
 371				startActivity(new Intent(this, ManageAccountActivity.class));
 372				break;
 373			case android.R.id.home:
 374				finish();
 375				break;
 376			case R.id.action_show_qr_code:
 377				showQrCode();
 378				break;
 379		}
 380		return super.onOptionsItemSelected(item);
 381	}
 382
 383	@Override
 384	protected void onCreate(Bundle savedInstanceState) {
 385		super.onCreate(savedInstanceState);
 386		metrics = getResources().getDisplayMetrics();
 387		ExceptionHelper.init(getApplicationContext());
 388
 389		mPrimaryTextColor = getResources().getColor(R.color.black87);
 390		mSecondaryTextColor = getResources().getColor(R.color.black54);
 391		mTertiaryTextColor = getResources().getColor(R.color.black12);
 392		mColorRed = getResources().getColor(R.color.red800);
 393		mColorOrange = getResources().getColor(R.color.orange500);
 394		mColorGreen = getResources().getColor(R.color.green500);
 395		mPrimaryColor = getResources().getColor(R.color.primary500);
 396		mPrimaryBackgroundColor = getResources().getColor(R.color.grey50);
 397		mSecondaryBackgroundColor = getResources().getColor(R.color.grey200);
 398
 399		if(isDarkTheme()) {
 400			mPrimaryTextColor = getResources().getColor(R.color.white);
 401			mSecondaryTextColor = getResources().getColor(R.color.white70);
 402			mTertiaryTextColor = getResources().getColor(R.color.white12);
 403			mPrimaryBackgroundColor = getResources().getColor(R.color.grey800);
 404			mSecondaryBackgroundColor = getResources().getColor(R.color.grey900);
 405		}
 406
 407		this.mTheme = findTheme();
 408		setTheme(this.mTheme);
 409
 410		this.mUsingEnterKey = usingEnterKey();
 411		mUseSubject = getPreferences().getBoolean("use_subject", true);
 412		final ActionBar ab = getActionBar();
 413		if (ab!=null) {
 414			ab.setDisplayHomeAsUpEnabled(true);
 415		}
 416	}
 417
 418	public boolean isDarkTheme() {
 419		return getPreferences().getString("theme", "light").equals("dark");
 420	}
 421
 422	public int getThemeResource(int r_attr_name, int r_drawable_def) {
 423		int[] attrs = {	r_attr_name };
 424		TypedArray ta = this.getTheme().obtainStyledAttributes(attrs);
 425
 426		int res = ta.getResourceId(0, r_drawable_def);
 427		ta.recycle();
 428
 429		return res;
 430	}
 431
 432	protected boolean isOptimizingBattery() {
 433		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 434			PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
 435			return !pm.isIgnoringBatteryOptimizations(getPackageName());
 436		} else {
 437			return false;
 438		}
 439	}
 440
 441	protected boolean usingEnterKey() {
 442		return getPreferences().getBoolean("display_enter_key", false);
 443	}
 444
 445	protected SharedPreferences getPreferences() {
 446		return PreferenceManager
 447			.getDefaultSharedPreferences(getApplicationContext());
 448	}
 449
 450	public boolean useSubjectToIdentifyConference() {
 451		return mUseSubject;
 452	}
 453
 454	public void switchToConversation(Conversation conversation) {
 455		switchToConversation(conversation, null, false);
 456	}
 457
 458	public void switchToConversation(Conversation conversation, String text,
 459			boolean newTask) {
 460		switchToConversation(conversation,text,null,false,newTask);
 461	}
 462
 463	public void highlightInMuc(Conversation conversation, String nick) {
 464		switchToConversation(conversation, null, nick, false, false);
 465	}
 466
 467	public void privateMsgInMuc(Conversation conversation, String nick) {
 468		switchToConversation(conversation, null, nick, true, false);
 469	}
 470
 471	private void switchToConversation(Conversation conversation, String text, String nick, boolean pm, boolean newTask) {
 472		Intent viewConversationIntent = new Intent(this,
 473				ConversationActivity.class);
 474		viewConversationIntent.setAction(ConversationActivity.ACTION_VIEW_CONVERSATION);
 475		viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
 476				conversation.getUuid());
 477		if (text != null) {
 478			viewConversationIntent.putExtra(ConversationActivity.TEXT, text);
 479		}
 480		if (nick != null) {
 481			viewConversationIntent.putExtra(ConversationActivity.NICK, nick);
 482			viewConversationIntent.putExtra(ConversationActivity.PRIVATE_MESSAGE,pm);
 483		}
 484		if (newTask) {
 485			viewConversationIntent.setFlags(viewConversationIntent.getFlags()
 486					| Intent.FLAG_ACTIVITY_NEW_TASK
 487					| Intent.FLAG_ACTIVITY_SINGLE_TOP);
 488		} else {
 489			viewConversationIntent.setFlags(viewConversationIntent.getFlags()
 490					| Intent.FLAG_ACTIVITY_CLEAR_TOP);
 491		}
 492		startActivity(viewConversationIntent);
 493		finish();
 494	}
 495
 496	public void switchToContactDetails(Contact contact) {
 497		switchToContactDetails(contact, null);
 498	}
 499
 500	public void switchToContactDetails(Contact contact, String messageFingerprint) {
 501		Intent intent = new Intent(this, ContactDetailsActivity.class);
 502		intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
 503		intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().toBareJid().toString());
 504		intent.putExtra("contact", contact.getJid().toString());
 505		intent.putExtra("fingerprint", messageFingerprint);
 506		startActivity(intent);
 507	}
 508
 509	public void switchToAccount(Account account) {
 510		switchToAccount(account, false);
 511	}
 512
 513	public void switchToAccount(Account account, boolean init) {
 514		Intent intent = new Intent(this, EditAccountActivity.class);
 515		intent.putExtra("jid", account.getJid().toBareJid().toString());
 516		intent.putExtra("init", init);
 517		startActivity(intent);
 518	}
 519
 520	protected void inviteToConversation(Conversation conversation) {
 521		Intent intent = new Intent(getApplicationContext(),
 522				ChooseContactActivity.class);
 523		List<String> contacts = new ArrayList<>();
 524		if (conversation.getMode() == Conversation.MODE_MULTI) {
 525			for (MucOptions.User user : conversation.getMucOptions().getUsers(false)) {
 526				Jid jid = user.getRealJid();
 527				if (jid != null) {
 528					contacts.add(jid.toBareJid().toString());
 529				}
 530			}
 531		} else {
 532			contacts.add(conversation.getJid().toBareJid().toString());
 533		}
 534		intent.putExtra("filter_contacts", contacts.toArray(new String[contacts.size()]));
 535		intent.putExtra("conversation", conversation.getUuid());
 536		intent.putExtra("multiple", true);
 537		intent.putExtra("show_enter_jid", true);
 538		intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().toBareJid().toString());
 539		startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION);
 540	}
 541
 542	protected void announcePgp(Account account, final Conversation conversation, final Runnable onSuccess) {
 543		if (account.getPgpId() == 0) {
 544			choosePgpSignId(account);
 545		} else {
 546			String status = null;
 547			if (manuallyChangePresence()) {
 548				status = account.getPresenceStatusMessage();
 549			}
 550			if (status == null) {
 551				status = "";
 552			}
 553			xmppConnectionService.getPgpEngine().generateSignature(account, status, new UiCallback<Account>() {
 554
 555				@Override
 556				public void userInputRequried(PendingIntent pi, Account account) {
 557					try {
 558						startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
 559					} catch (final SendIntentException ignored) {
 560					}
 561				}
 562
 563				@Override
 564				public void success(Account account) {
 565					xmppConnectionService.databaseBackend.updateAccount(account);
 566					xmppConnectionService.sendPresence(account);
 567					if (conversation != null) {
 568						conversation.setNextEncryption(Message.ENCRYPTION_PGP);
 569						xmppConnectionService.databaseBackend.updateConversation(conversation);
 570						refreshUi();
 571					}
 572					if (onSuccess != null) {
 573						runOnUiThread(onSuccess);
 574					}
 575				}
 576
 577				@Override
 578				public void error(int error, Account account) {
 579					displayErrorDialog(error);
 580				}
 581			});
 582		}
 583	}
 584
 585	protected  boolean noAccountUsesPgp() {
 586		if (!hasPgp()) {
 587			return true;
 588		}
 589		for(Account account : xmppConnectionService.getAccounts()) {
 590			if (account.getPgpId() != 0) {
 591				return false;
 592			}
 593		}
 594		return true;
 595	}
 596
 597	@SuppressWarnings("deprecation")
 598	@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
 599	protected void setListItemBackgroundOnView(View view) {
 600		int sdk = android.os.Build.VERSION.SDK_INT;
 601		if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) {
 602			view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground));
 603		} else {
 604			view.setBackground(getResources().getDrawable(R.drawable.greybackground));
 605		}
 606	}
 607
 608	protected void choosePgpSignId(Account account) {
 609		xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<Account>() {
 610			@Override
 611			public void success(Account account1) {
 612			}
 613
 614			@Override
 615			public void error(int errorCode, Account object) {
 616
 617			}
 618
 619			@Override
 620			public void userInputRequried(PendingIntent pi, Account object) {
 621				try {
 622					startIntentSenderForResult(pi.getIntentSender(),
 623							REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0);
 624				} catch (final SendIntentException ignored) {
 625				}
 626			}
 627		});
 628	}
 629
 630	protected void displayErrorDialog(final int errorCode) {
 631		runOnUiThread(new Runnable() {
 632
 633			@Override
 634			public void run() {
 635				AlertDialog.Builder builder = new AlertDialog.Builder(
 636						XmppActivity.this);
 637				builder.setIconAttribute(android.R.attr.alertDialogIcon);
 638				builder.setTitle(getString(R.string.error));
 639				builder.setMessage(errorCode);
 640				builder.setNeutralButton(R.string.accept, null);
 641				builder.create().show();
 642			}
 643		});
 644
 645	}
 646
 647	protected void showAddToRosterDialog(final Conversation conversation) {
 648		showAddToRosterDialog(conversation.getContact());
 649	}
 650
 651	protected void showAddToRosterDialog(final Contact contact) {
 652		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 653		builder.setTitle(contact.getJid().toString());
 654		builder.setMessage(getString(R.string.not_in_roster));
 655		builder.setNegativeButton(getString(R.string.cancel), null);
 656		builder.setPositiveButton(getString(R.string.add_contact),
 657				new DialogInterface.OnClickListener() {
 658
 659					@Override
 660					public void onClick(DialogInterface dialog, int which) {
 661						final Jid jid = contact.getJid();
 662						Account account = contact.getAccount();
 663						Contact contact = account.getRoster().getContact(jid);
 664						xmppConnectionService.createContact(contact);
 665					}
 666				});
 667		builder.create().show();
 668	}
 669
 670	private void showAskForPresenceDialog(final Contact contact) {
 671		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 672		builder.setTitle(contact.getJid().toString());
 673		builder.setMessage(R.string.request_presence_updates);
 674		builder.setNegativeButton(R.string.cancel, null);
 675		builder.setPositiveButton(R.string.request_now,
 676				new DialogInterface.OnClickListener() {
 677
 678					@Override
 679					public void onClick(DialogInterface dialog, int which) {
 680						if (xmppConnectionServiceBound) {
 681							xmppConnectionService.sendPresencePacket(contact
 682									.getAccount(), xmppConnectionService
 683									.getPresenceGenerator()
 684									.requestPresenceUpdatesFrom(contact));
 685						}
 686					}
 687				});
 688		builder.create().show();
 689	}
 690
 691	private void warnMutalPresenceSubscription(final Conversation conversation,
 692			final OnPresenceSelected listener) {
 693		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 694		builder.setTitle(conversation.getContact().getJid().toString());
 695		builder.setMessage(R.string.without_mutual_presence_updates);
 696		builder.setNegativeButton(R.string.cancel, null);
 697		builder.setPositiveButton(R.string.ignore, new OnClickListener() {
 698
 699			@Override
 700			public void onClick(DialogInterface dialog, int which) {
 701				conversation.setNextCounterpart(null);
 702				if (listener != null) {
 703					listener.onPresenceSelected();
 704				}
 705			}
 706		});
 707		builder.create().show();
 708	}
 709
 710	protected void quickEdit(String previousValue, int hint, OnValueEdited callback) {
 711		quickEdit(previousValue, callback, hint, false);
 712	}
 713
 714	protected void quickPasswordEdit(String previousValue, OnValueEdited callback) {
 715		quickEdit(previousValue, callback, R.string.password, true);
 716	}
 717
 718	@SuppressLint("InflateParams")
 719	private void quickEdit(final String previousValue,
 720						   final OnValueEdited callback,
 721						   final int hint,
 722						   boolean password) {
 723		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 724		View view = getLayoutInflater().inflate(R.layout.quickedit, null);
 725		final EditText editor = (EditText) view.findViewById(R.id.editor);
 726		OnClickListener mClickListener = new OnClickListener() {
 727
 728			@Override
 729			public void onClick(DialogInterface dialog, int which) {
 730				String value = editor.getText().toString();
 731				if (!value.equals(previousValue) && value.trim().length() > 0) {
 732					callback.onValueEdited(value);
 733				}
 734			}
 735		};
 736		if (password) {
 737			editor.setInputType(InputType.TYPE_CLASS_TEXT
 738					| InputType.TYPE_TEXT_VARIATION_PASSWORD);
 739			builder.setPositiveButton(R.string.accept, mClickListener);
 740		} else {
 741			builder.setPositiveButton(R.string.edit, mClickListener);
 742		}
 743		if (hint != 0) {
 744			editor.setHint(hint);
 745		}
 746		editor.requestFocus();
 747		editor.setText("");
 748		if (previousValue != null) {
 749			editor.getText().append(previousValue);
 750		}
 751		builder.setView(view);
 752		builder.setNegativeButton(R.string.cancel, null);
 753		builder.create().show();
 754	}
 755
 756	protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight, View.OnClickListener onKeyClickedListener) {
 757		final XmppAxolotlSession.Trust trust = account.getAxolotlService()
 758				.getFingerprintTrust(fingerprint);
 759		if (trust == null) {
 760			return false;
 761		}
 762		return addFingerprintRowWithListeners(keys, account, fingerprint, highlight, trust, true,
 763				new CompoundButton.OnCheckedChangeListener() {
 764					@Override
 765					public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
 766						account.getAxolotlService().setFingerprintTrust(fingerprint,
 767								(isChecked) ? XmppAxolotlSession.Trust.TRUSTED :
 768										XmppAxolotlSession.Trust.UNTRUSTED);
 769					}
 770				},
 771				new View.OnClickListener() {
 772					@Override
 773					public void onClick(View v) {
 774						account.getAxolotlService().setFingerprintTrust(fingerprint,
 775								XmppAxolotlSession.Trust.UNTRUSTED);
 776						v.setEnabled(true);
 777					}
 778				},
 779				onKeyClickedListener
 780
 781		);
 782	}
 783
 784	protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account,
 785	                                                 final String fingerprint,
 786	                                                 boolean highlight,
 787	                                                 XmppAxolotlSession.Trust trust,
 788	                                                 boolean showTag,
 789	                                                 CompoundButton.OnCheckedChangeListener
 790			                                                 onCheckedChangeListener,
 791	                                                 View.OnClickListener onClickListener,
 792													 View.OnClickListener onKeyClickedListener) {
 793		if (trust == XmppAxolotlSession.Trust.COMPROMISED) {
 794			return false;
 795		}
 796		View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
 797		TextView key = (TextView) view.findViewById(R.id.key);
 798		key.setOnClickListener(onKeyClickedListener);
 799		TextView keyType = (TextView) view.findViewById(R.id.key_type);
 800		keyType.setOnClickListener(onKeyClickedListener);
 801		Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
 802		trustToggle.setVisibility(View.VISIBLE);
 803		trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
 804		trustToggle.setOnClickListener(onClickListener);
 805		final View.OnLongClickListener purge = new View.OnLongClickListener() {
 806			@Override
 807			public boolean onLongClick(View v) {
 808				showPurgeKeyDialog(account, fingerprint);
 809				return true;
 810			}
 811		};
 812		boolean active = true;
 813		view.setOnLongClickListener(purge);
 814		key.setOnLongClickListener(purge);
 815		keyType.setOnLongClickListener(purge);
 816		boolean x509 = Config.X509_VERIFICATION
 817				&& (trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509);
 818		switch (trust) {
 819			case UNTRUSTED:
 820			case TRUSTED:
 821			case TRUSTED_X509:
 822				trustToggle.setChecked(trust.trusted(), false);
 823				trustToggle.setEnabled(!Config.X509_VERIFICATION || trust != XmppAxolotlSession.Trust.TRUSTED_X509);
 824				if (Config.X509_VERIFICATION && trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
 825					trustToggle.setOnClickListener(null);
 826				}
 827				key.setTextColor(getPrimaryTextColor());
 828				keyType.setTextColor(getSecondaryTextColor());
 829				break;
 830			case UNDECIDED:
 831				trustToggle.setChecked(false, false);
 832				trustToggle.setEnabled(false);
 833				key.setTextColor(getPrimaryTextColor());
 834				keyType.setTextColor(getSecondaryTextColor());
 835				break;
 836			case INACTIVE_UNTRUSTED:
 837			case INACTIVE_UNDECIDED:
 838				trustToggle.setOnClickListener(null);
 839				trustToggle.setChecked(false, false);
 840				trustToggle.setEnabled(false);
 841				key.setTextColor(getTertiaryTextColor());
 842				keyType.setTextColor(getTertiaryTextColor());
 843				active = false;
 844				break;
 845			case INACTIVE_TRUSTED:
 846			case INACTIVE_TRUSTED_X509:
 847				trustToggle.setOnClickListener(null);
 848				trustToggle.setChecked(true, false);
 849				trustToggle.setEnabled(false);
 850				key.setTextColor(getTertiaryTextColor());
 851				keyType.setTextColor(getTertiaryTextColor());
 852				active = false;
 853				break;
 854		}
 855
 856		if (showTag) {
 857			keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
 858		} else {
 859			keyType.setVisibility(View.GONE);
 860		}
 861		if (highlight) {
 862			keyType.setTextColor(getResources().getColor(R.color.accent));
 863			keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
 864		} else {
 865			keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
 866		}
 867
 868		key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
 869
 870		final View.OnClickListener toast;
 871		if (!active) {
 872			toast = new View.OnClickListener() {
 873				@Override
 874				public void onClick(View v) {
 875					replaceToast(getString(R.string.this_device_is_no_longer_in_use), false);
 876				}
 877			};
 878			trustToggle.setOnClickListener(toast);
 879		} else {
 880			toast = new View.OnClickListener() {
 881				@Override
 882				public void onClick(View v) {
 883					hideToast();
 884				}
 885			};
 886		}
 887		view.setOnClickListener(toast);
 888		key.setOnClickListener(toast);
 889		keyType.setOnClickListener(toast);
 890
 891		keys.addView(view);
 892		return true;
 893	}
 894
 895	public void showPurgeKeyDialog(final Account account, final String fingerprint) {
 896		Builder builder = new Builder(this);
 897		builder.setTitle(getString(R.string.purge_key));
 898		builder.setIconAttribute(android.R.attr.alertDialogIcon);
 899		builder.setMessage(getString(R.string.purge_key_desc_part1)
 900				+ "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2))
 901				+ "\n\n" + getString(R.string.purge_key_desc_part2));
 902		builder.setNegativeButton(getString(R.string.cancel), null);
 903		builder.setPositiveButton(getString(R.string.purge_key),
 904				new DialogInterface.OnClickListener() {
 905					@Override
 906					public void onClick(DialogInterface dialog, int which) {
 907						account.getAxolotlService().purgeKey(fingerprint);
 908						refreshUi();
 909					}
 910				});
 911		builder.create().show();
 912	}
 913
 914	public boolean hasStoragePermission(int requestCode) {
 915		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 916			if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
 917				requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
 918				return false;
 919			} else {
 920				return true;
 921			}
 922		} else {
 923			return true;
 924		}
 925	}
 926
 927	public void selectPresence(final Conversation conversation,
 928			final OnPresenceSelected listener) {
 929		final Contact contact = conversation.getContact();
 930		if (conversation.hasValidOtrSession()) {
 931			SessionID id = conversation.getOtrSession().getSessionID();
 932			Jid jid;
 933			try {
 934				jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID());
 935			} catch (InvalidJidException e) {
 936				jid = null;
 937			}
 938			conversation.setNextCounterpart(jid);
 939			listener.onPresenceSelected();
 940		} else 	if (!contact.showInRoster()) {
 941			showAddToRosterDialog(conversation);
 942		} else {
 943			Presences presences = contact.getPresences();
 944			if (presences.size() == 0) {
 945				if (!contact.getOption(Contact.Options.TO)
 946						&& !contact.getOption(Contact.Options.ASKING)
 947						&& contact.getAccount().getStatus() == Account.State.ONLINE) {
 948					showAskForPresenceDialog(contact);
 949				} else if (!contact.getOption(Contact.Options.TO)
 950						|| !contact.getOption(Contact.Options.FROM)) {
 951					warnMutalPresenceSubscription(conversation, listener);
 952				} else {
 953					conversation.setNextCounterpart(null);
 954					listener.onPresenceSelected();
 955				}
 956			} else if (presences.size() == 1) {
 957				String presence = presences.asStringArray()[0];
 958				try {
 959					conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence));
 960				} catch (InvalidJidException e) {
 961					conversation.setNextCounterpart(null);
 962				}
 963				listener.onPresenceSelected();
 964			} else {
 965				final StringBuilder presence = new StringBuilder();
 966				AlertDialog.Builder builder = new AlertDialog.Builder(this);
 967				builder.setTitle(getString(R.string.choose_presence));
 968				final String[] presencesArray = presences.asStringArray();
 969				int preselectedPresence = 0;
 970				for (int i = 0; i < presencesArray.length; ++i) {
 971					if (presencesArray[i].equals(contact.getLastPresence())) {
 972						preselectedPresence = i;
 973						break;
 974					}
 975				}
 976				presence.append(presencesArray[preselectedPresence]);
 977				builder.setSingleChoiceItems(presencesArray,
 978						preselectedPresence,
 979						new DialogInterface.OnClickListener() {
 980
 981							@Override
 982							public void onClick(DialogInterface dialog,
 983									int which) {
 984								presence.delete(0, presence.length());
 985								presence.append(presencesArray[which]);
 986							}
 987						});
 988				builder.setNegativeButton(R.string.cancel, null);
 989				builder.setPositiveButton(R.string.ok, new OnClickListener() {
 990
 991					@Override
 992					public void onClick(DialogInterface dialog, int which) {
 993						try {
 994							conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString()));
 995						} catch (InvalidJidException e) {
 996							conversation.setNextCounterpart(null);
 997						}
 998						listener.onPresenceSelected();
 999					}
1000				});
1001				builder.create().show();
1002			}
1003		}
1004	}
1005
1006	protected void onActivityResult(int requestCode, int resultCode,
1007			final Intent data) {
1008		super.onActivityResult(requestCode, resultCode, data);
1009		if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
1010			mPendingConferenceInvite = ConferenceInvite.parse(data);
1011			if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
1012				if (mPendingConferenceInvite.execute(this)) {
1013					mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
1014					mToast.show();
1015				}
1016				mPendingConferenceInvite = null;
1017			}
1018		}
1019	}
1020
1021
1022	private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
1023		@Override
1024		public void success(final Conversation conversation) {
1025			runOnUiThread(new Runnable() {
1026				@Override
1027				public void run() {
1028					switchToConversation(conversation);
1029					hideToast();
1030				}
1031			});
1032		}
1033
1034		@Override
1035		public void error(final int errorCode, Conversation object) {
1036			runOnUiThread(new Runnable() {
1037				@Override
1038				public void run() {
1039					replaceToast(getString(errorCode));
1040				}
1041			});
1042		}
1043
1044		@Override
1045		public void userInputRequried(PendingIntent pi, Conversation object) {
1046
1047		}
1048	};
1049
1050	public int getTertiaryTextColor() {
1051		return this.mTertiaryTextColor;
1052	}
1053
1054	public int getSecondaryTextColor() {
1055		return this.mSecondaryTextColor;
1056	}
1057
1058	public int getPrimaryTextColor() {
1059		return this.mPrimaryTextColor;
1060	}
1061
1062	public int getWarningTextColor() {
1063		return this.mColorRed;
1064	}
1065
1066	public int getOnlineColor() {
1067		return this.mColorGreen;
1068	}
1069
1070	public int getPrimaryBackgroundColor() {
1071		return this.mPrimaryBackgroundColor;
1072	}
1073
1074	public int getSecondaryBackgroundColor() {
1075		return this.mSecondaryBackgroundColor;
1076	}
1077
1078	public int getPixel(int dp) {
1079		DisplayMetrics metrics = getResources().getDisplayMetrics();
1080		return ((int) (dp * metrics.density));
1081	}
1082
1083	public boolean copyTextToClipboard(String text, int labelResId) {
1084		ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
1085		String label = getResources().getString(labelResId);
1086		if (mClipBoardManager != null) {
1087			ClipData mClipData = ClipData.newPlainText(label, text);
1088			mClipBoardManager.setPrimaryClip(mClipData);
1089			return true;
1090		}
1091		return false;
1092	}
1093
1094	protected void registerNdefPushMessageCallback() {
1095		NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
1096		if (nfcAdapter != null && nfcAdapter.isEnabled()) {
1097			nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
1098				@Override
1099				public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
1100					return new NdefMessage(new NdefRecord[]{
1101							NdefRecord.createUri(getShareableUri()),
1102							NdefRecord.createApplicationRecord("eu.siacs.conversations")
1103					});
1104				}
1105			}, this);
1106		}
1107	}
1108
1109	protected boolean neverCompressPictures() {
1110		return getPreferences().getString("picture_compression", "auto").equals("never");
1111	}
1112
1113	protected boolean manuallyChangePresence() {
1114		return getPreferences().getBoolean("manually_change_presence", false);
1115	}
1116
1117	protected void unregisterNdefPushMessageCallback() {
1118		NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
1119		if (nfcAdapter != null && nfcAdapter.isEnabled()) {
1120			nfcAdapter.setNdefPushMessageCallback(null,this);
1121		}
1122	}
1123
1124	protected String getShareableUri() {
1125		return null;
1126	}
1127
1128	protected void shareUri() {
1129		String uri = getShareableUri();
1130		if (uri == null || uri.isEmpty()) {
1131			return;
1132		}
1133		Intent shareIntent = new Intent();
1134		shareIntent.setAction(Intent.ACTION_SEND);
1135		shareIntent.putExtra(Intent.EXTRA_TEXT, getShareableUri());
1136		shareIntent.setType("text/plain");
1137		try {
1138			startActivity(Intent.createChooser(shareIntent, getText(R.string.share_uri_with)));
1139		} catch (ActivityNotFoundException e) {
1140			Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
1141		}
1142	}
1143
1144	@Override
1145	public void onResume() {
1146		super.onResume();
1147		if (this.getShareableUri()!=null) {
1148			this.registerNdefPushMessageCallback();
1149		}
1150	}
1151
1152	protected int findTheme() {
1153		Boolean dark   = getPreferences().getString("theme", "light").equals("dark");
1154		Boolean larger = getPreferences().getBoolean("use_larger_font", false);
1155
1156		if(dark) {
1157			if(larger)
1158				return R.style.ConversationsTheme_Dark_LargerText;
1159			else
1160				return R.style.ConversationsTheme_Dark;
1161		} else {
1162			if (larger)
1163				return R.style.ConversationsTheme_LargerText;
1164			else
1165				return R.style.ConversationsTheme;
1166		}
1167	}
1168
1169	@Override
1170	public void onPause() {
1171		super.onPause();
1172		this.unregisterNdefPushMessageCallback();
1173	}
1174
1175	protected void showQrCode() {
1176		String uri = getShareableUri();
1177		if (uri!=null) {
1178			Point size = new Point();
1179			getWindowManager().getDefaultDisplay().getSize(size);
1180			final int width = (size.x < size.y ? size.x : size.y);
1181			Bitmap bitmap = createQrCodeBitmap(uri, width);
1182			ImageView view = new ImageView(this);
1183			view.setBackgroundColor(Color.WHITE);
1184			view.setImageBitmap(bitmap);
1185			AlertDialog.Builder builder = new AlertDialog.Builder(this);
1186			builder.setView(view);
1187			builder.create().show();
1188		}
1189	}
1190
1191	protected Bitmap createQrCodeBitmap(String input, int size) {
1192		Log.d(Config.LOGTAG,"qr code requested size: "+size);
1193		try {
1194			final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
1195			final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
1196			hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
1197			final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
1198			final int width = result.getWidth();
1199			final int height = result.getHeight();
1200			final int[] pixels = new int[width * height];
1201			for (int y = 0; y < height; y++) {
1202				final int offset = y * width;
1203				for (int x = 0; x < width; x++) {
1204					pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
1205				}
1206			}
1207			final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1208			Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
1209			bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
1210			return bitmap;
1211		} catch (final WriterException e) {
1212			return null;
1213		}
1214	}
1215
1216	protected Account extractAccount(Intent intent) {
1217		String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
1218		try {
1219			return jid != null ? xmppConnectionService.findAccountByJid(Jid.fromString(jid)) : null;
1220		} catch (InvalidJidException e) {
1221			return null;
1222		}
1223	}
1224
1225	public static class ConferenceInvite {
1226		private String uuid;
1227		private List<Jid> jids = new ArrayList<>();
1228
1229		public static ConferenceInvite parse(Intent data) {
1230			ConferenceInvite invite = new ConferenceInvite();
1231			invite.uuid = data.getStringExtra("conversation");
1232			if (invite.uuid == null) {
1233				return null;
1234			}
1235			try {
1236				if (data.getBooleanExtra("multiple", false)) {
1237					String[] toAdd = data.getStringArrayExtra("contacts");
1238					for (String item : toAdd) {
1239						invite.jids.add(Jid.fromString(item));
1240					}
1241				} else {
1242					invite.jids.add(Jid.fromString(data.getStringExtra("contact")));
1243				}
1244			} catch (final InvalidJidException ignored) {
1245				return null;
1246			}
1247			return invite;
1248		}
1249
1250		public boolean execute(XmppActivity activity) {
1251			XmppConnectionService service = activity.xmppConnectionService;
1252			Conversation conversation = service.findConversationByUuid(this.uuid);
1253			if (conversation == null) {
1254				return false;
1255			}
1256			if (conversation.getMode() == Conversation.MODE_MULTI) {
1257				for (Jid jid : jids) {
1258					service.invite(conversation, jid);
1259				}
1260				return false;
1261			} else {
1262				jids.add(conversation.getJid().toBareJid());
1263				service.createAdhocConference(conversation.getAccount(), null, jids, activity.adhocCallback);
1264				return true;
1265			}
1266		}
1267	}
1268
1269	public AvatarService avatarService() {
1270		return xmppConnectionService.getAvatarService();
1271	}
1272
1273	class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
1274		private final WeakReference<ImageView> imageViewReference;
1275		private Message message = null;
1276
1277		public BitmapWorkerTask(ImageView imageView) {
1278			imageViewReference = new WeakReference<>(imageView);
1279		}
1280
1281		@Override
1282		protected Bitmap doInBackground(Message... params) {
1283			if (isCancelled()) {
1284				return null;
1285			}
1286			message = params[0];
1287			try {
1288				return xmppConnectionService.getFileBackend().getThumbnail(
1289						message, (int) (metrics.density * 288), false);
1290			} catch (FileNotFoundException e) {
1291				return null;
1292			}
1293		}
1294
1295		@Override
1296		protected void onPostExecute(Bitmap bitmap) {
1297			if (bitmap != null && !isCancelled()) {
1298				final ImageView imageView = imageViewReference.get();
1299				if (imageView != null) {
1300					imageView.setImageBitmap(bitmap);
1301					imageView.setBackgroundColor(0x00000000);
1302				}
1303			}
1304		}
1305	}
1306
1307	public void loadBitmap(Message message, ImageView imageView) {
1308		Bitmap bm;
1309		try {
1310			bm = xmppConnectionService.getFileBackend().getThumbnail(message,
1311					(int) (metrics.density * 288), true);
1312		} catch (FileNotFoundException e) {
1313			bm = null;
1314		}
1315		if (bm != null) {
1316			cancelPotentialWork(message, imageView);
1317			imageView.setImageBitmap(bm);
1318			imageView.setBackgroundColor(0x00000000);
1319		} else {
1320			if (cancelPotentialWork(message, imageView)) {
1321				imageView.setBackgroundColor(0xff333333);
1322				imageView.setImageDrawable(null);
1323				final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1324				final AsyncDrawable asyncDrawable = new AsyncDrawable(
1325						getResources(), null, task);
1326				imageView.setImageDrawable(asyncDrawable);
1327				try {
1328					task.execute(message);
1329				} catch (final RejectedExecutionException ignored) {
1330					ignored.printStackTrace();
1331				}
1332			}
1333		}
1334	}
1335
1336	public static boolean cancelPotentialWork(Message message, ImageView imageView) {
1337		final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
1338
1339		if (bitmapWorkerTask != null) {
1340			final Message oldMessage = bitmapWorkerTask.message;
1341			if (oldMessage == null || message != oldMessage) {
1342				bitmapWorkerTask.cancel(true);
1343			} else {
1344				return false;
1345			}
1346		}
1347		return true;
1348	}
1349
1350	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
1351		if (imageView != null) {
1352			final Drawable drawable = imageView.getDrawable();
1353			if (drawable instanceof AsyncDrawable) {
1354				final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
1355				return asyncDrawable.getBitmapWorkerTask();
1356			}
1357		}
1358		return null;
1359	}
1360
1361	static class AsyncDrawable extends BitmapDrawable {
1362		private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1363
1364		public AsyncDrawable(Resources res, Bitmap bitmap,
1365				BitmapWorkerTask bitmapWorkerTask) {
1366			super(res, bitmap);
1367			bitmapWorkerTaskReference = new WeakReference<>(
1368					bitmapWorkerTask);
1369		}
1370
1371		public BitmapWorkerTask getBitmapWorkerTask() {
1372			return bitmapWorkerTaskReference.get();
1373		}
1374	}
1375}