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