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					}
 567					if (onSuccess != null) {
 568						runOnUiThread(onSuccess);
 569					}
 570				}
 571
 572				@Override
 573				public void error(int error, Account account) {
 574					displayErrorDialog(error);
 575				}
 576			});
 577		}
 578	}
 579
 580	protected  boolean noAccountUsesPgp() {
 581		if (!hasPgp()) {
 582			return true;
 583		}
 584		for(Account account : xmppConnectionService.getAccounts()) {
 585			if (account.getPgpId() != 0) {
 586				return false;
 587			}
 588		}
 589		return true;
 590	}
 591
 592	@SuppressWarnings("deprecation")
 593	@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
 594	protected void setListItemBackgroundOnView(View view) {
 595		int sdk = android.os.Build.VERSION.SDK_INT;
 596		if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) {
 597			view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground));
 598		} else {
 599			view.setBackground(getResources().getDrawable(R.drawable.greybackground));
 600		}
 601	}
 602
 603	protected void choosePgpSignId(Account account) {
 604		xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<Account>() {
 605			@Override
 606			public void success(Account account1) {
 607			}
 608
 609			@Override
 610			public void error(int errorCode, Account object) {
 611
 612			}
 613
 614			@Override
 615			public void userInputRequried(PendingIntent pi, Account object) {
 616				try {
 617					startIntentSenderForResult(pi.getIntentSender(),
 618							REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0);
 619				} catch (final SendIntentException ignored) {
 620				}
 621			}
 622		});
 623	}
 624
 625	protected void displayErrorDialog(final int errorCode) {
 626		runOnUiThread(new Runnable() {
 627
 628			@Override
 629			public void run() {
 630				AlertDialog.Builder builder = new AlertDialog.Builder(
 631						XmppActivity.this);
 632				builder.setIconAttribute(android.R.attr.alertDialogIcon);
 633				builder.setTitle(getString(R.string.error));
 634				builder.setMessage(errorCode);
 635				builder.setNeutralButton(R.string.accept, null);
 636				builder.create().show();
 637			}
 638		});
 639
 640	}
 641
 642	protected void showAddToRosterDialog(final Conversation conversation) {
 643		showAddToRosterDialog(conversation.getContact());
 644	}
 645
 646	protected void showAddToRosterDialog(final Contact contact) {
 647		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 648		builder.setTitle(contact.getJid().toString());
 649		builder.setMessage(getString(R.string.not_in_roster));
 650		builder.setNegativeButton(getString(R.string.cancel), null);
 651		builder.setPositiveButton(getString(R.string.add_contact),
 652				new DialogInterface.OnClickListener() {
 653
 654					@Override
 655					public void onClick(DialogInterface dialog, int which) {
 656						final Jid jid = contact.getJid();
 657						Account account = contact.getAccount();
 658						Contact contact = account.getRoster().getContact(jid);
 659						xmppConnectionService.createContact(contact);
 660					}
 661				});
 662		builder.create().show();
 663	}
 664
 665	private void showAskForPresenceDialog(final Contact contact) {
 666		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 667		builder.setTitle(contact.getJid().toString());
 668		builder.setMessage(R.string.request_presence_updates);
 669		builder.setNegativeButton(R.string.cancel, null);
 670		builder.setPositiveButton(R.string.request_now,
 671				new DialogInterface.OnClickListener() {
 672
 673					@Override
 674					public void onClick(DialogInterface dialog, int which) {
 675						if (xmppConnectionServiceBound) {
 676							xmppConnectionService.sendPresencePacket(contact
 677									.getAccount(), xmppConnectionService
 678									.getPresenceGenerator()
 679									.requestPresenceUpdatesFrom(contact));
 680						}
 681					}
 682				});
 683		builder.create().show();
 684	}
 685
 686	private void warnMutalPresenceSubscription(final Conversation conversation,
 687			final OnPresenceSelected listener) {
 688		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 689		builder.setTitle(conversation.getContact().getJid().toString());
 690		builder.setMessage(R.string.without_mutual_presence_updates);
 691		builder.setNegativeButton(R.string.cancel, null);
 692		builder.setPositiveButton(R.string.ignore, new OnClickListener() {
 693
 694			@Override
 695			public void onClick(DialogInterface dialog, int which) {
 696				conversation.setNextCounterpart(null);
 697				if (listener != null) {
 698					listener.onPresenceSelected();
 699				}
 700			}
 701		});
 702		builder.create().show();
 703	}
 704
 705	protected void quickEdit(String previousValue, int hint, OnValueEdited callback) {
 706		quickEdit(previousValue, callback, hint, false);
 707	}
 708
 709	protected void quickPasswordEdit(String previousValue, OnValueEdited callback) {
 710		quickEdit(previousValue, callback, R.string.password, true);
 711	}
 712
 713	@SuppressLint("InflateParams")
 714	private void quickEdit(final String previousValue,
 715						   final OnValueEdited callback,
 716						   final int hint,
 717						   boolean password) {
 718		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 719		View view = getLayoutInflater().inflate(R.layout.quickedit, null);
 720		final EditText editor = (EditText) view.findViewById(R.id.editor);
 721		OnClickListener mClickListener = new OnClickListener() {
 722
 723			@Override
 724			public void onClick(DialogInterface dialog, int which) {
 725				String value = editor.getText().toString();
 726				if (!value.equals(previousValue) && value.trim().length() > 0) {
 727					callback.onValueEdited(value);
 728				}
 729			}
 730		};
 731		if (password) {
 732			editor.setInputType(InputType.TYPE_CLASS_TEXT
 733					| InputType.TYPE_TEXT_VARIATION_PASSWORD);
 734			builder.setPositiveButton(R.string.accept, mClickListener);
 735		} else {
 736			builder.setPositiveButton(R.string.edit, mClickListener);
 737		}
 738		if (hint != 0) {
 739			editor.setHint(hint);
 740		}
 741		editor.requestFocus();
 742		editor.setText("");
 743		if (previousValue != null) {
 744			editor.getText().append(previousValue);
 745		}
 746		builder.setView(view);
 747		builder.setNegativeButton(R.string.cancel, null);
 748		builder.create().show();
 749	}
 750
 751	protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight, View.OnClickListener onKeyClickedListener) {
 752		final XmppAxolotlSession.Trust trust = account.getAxolotlService()
 753				.getFingerprintTrust(fingerprint);
 754		if (trust == null) {
 755			return false;
 756		}
 757		return addFingerprintRowWithListeners(keys, account, fingerprint, highlight, trust, true,
 758				new CompoundButton.OnCheckedChangeListener() {
 759					@Override
 760					public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
 761						account.getAxolotlService().setFingerprintTrust(fingerprint,
 762								(isChecked) ? XmppAxolotlSession.Trust.TRUSTED :
 763										XmppAxolotlSession.Trust.UNTRUSTED);
 764					}
 765				},
 766				new View.OnClickListener() {
 767					@Override
 768					public void onClick(View v) {
 769						account.getAxolotlService().setFingerprintTrust(fingerprint,
 770								XmppAxolotlSession.Trust.UNTRUSTED);
 771						v.setEnabled(true);
 772					}
 773				},
 774				onKeyClickedListener
 775
 776		);
 777	}
 778
 779	protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account,
 780	                                                 final String fingerprint,
 781	                                                 boolean highlight,
 782	                                                 XmppAxolotlSession.Trust trust,
 783	                                                 boolean showTag,
 784	                                                 CompoundButton.OnCheckedChangeListener
 785			                                                 onCheckedChangeListener,
 786	                                                 View.OnClickListener onClickListener,
 787													 View.OnClickListener onKeyClickedListener) {
 788		if (trust == XmppAxolotlSession.Trust.COMPROMISED) {
 789			return false;
 790		}
 791		View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
 792		TextView key = (TextView) view.findViewById(R.id.key);
 793		key.setOnClickListener(onKeyClickedListener);
 794		TextView keyType = (TextView) view.findViewById(R.id.key_type);
 795		keyType.setOnClickListener(onKeyClickedListener);
 796		Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
 797		trustToggle.setVisibility(View.VISIBLE);
 798		trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
 799		trustToggle.setOnClickListener(onClickListener);
 800		final View.OnLongClickListener purge = new View.OnLongClickListener() {
 801			@Override
 802			public boolean onLongClick(View v) {
 803				showPurgeKeyDialog(account, fingerprint);
 804				return true;
 805			}
 806		};
 807		view.setOnLongClickListener(purge);
 808		key.setOnLongClickListener(purge);
 809		keyType.setOnLongClickListener(purge);
 810		boolean x509 = Config.X509_VERIFICATION
 811				&& (trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509);
 812		switch (trust) {
 813			case UNTRUSTED:
 814			case TRUSTED:
 815			case TRUSTED_X509:
 816				trustToggle.setChecked(trust.trusted(), false);
 817				trustToggle.setEnabled(!Config.X509_VERIFICATION || trust != XmppAxolotlSession.Trust.TRUSTED_X509);
 818				if (Config.X509_VERIFICATION && trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
 819					trustToggle.setOnClickListener(null);
 820				}
 821				key.setTextColor(getPrimaryTextColor());
 822				keyType.setTextColor(getSecondaryTextColor());
 823				break;
 824			case UNDECIDED:
 825				trustToggle.setChecked(false, false);
 826				trustToggle.setEnabled(false);
 827				key.setTextColor(getPrimaryTextColor());
 828				keyType.setTextColor(getSecondaryTextColor());
 829				break;
 830			case INACTIVE_UNTRUSTED:
 831			case INACTIVE_UNDECIDED:
 832				trustToggle.setOnClickListener(null);
 833				trustToggle.setChecked(false, false);
 834				trustToggle.setEnabled(false);
 835				key.setTextColor(getTertiaryTextColor());
 836				keyType.setTextColor(getTertiaryTextColor());
 837				break;
 838			case INACTIVE_TRUSTED:
 839			case INACTIVE_TRUSTED_X509:
 840				trustToggle.setOnClickListener(null);
 841				trustToggle.setChecked(true, false);
 842				trustToggle.setEnabled(false);
 843				key.setTextColor(getTertiaryTextColor());
 844				keyType.setTextColor(getTertiaryTextColor());
 845				break;
 846		}
 847
 848		if (showTag) {
 849			keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
 850		} else {
 851			keyType.setVisibility(View.GONE);
 852		}
 853		if (highlight) {
 854			keyType.setTextColor(getResources().getColor(R.color.accent));
 855			keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
 856		} else {
 857			keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
 858		}
 859
 860		key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
 861		keys.addView(view);
 862		return true;
 863	}
 864
 865	public void showPurgeKeyDialog(final Account account, final String fingerprint) {
 866		Builder builder = new Builder(this);
 867		builder.setTitle(getString(R.string.purge_key));
 868		builder.setIconAttribute(android.R.attr.alertDialogIcon);
 869		builder.setMessage(getString(R.string.purge_key_desc_part1)
 870				+ "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2))
 871				+ "\n\n" + getString(R.string.purge_key_desc_part2));
 872		builder.setNegativeButton(getString(R.string.cancel), null);
 873		builder.setPositiveButton(getString(R.string.purge_key),
 874				new DialogInterface.OnClickListener() {
 875					@Override
 876					public void onClick(DialogInterface dialog, int which) {
 877						account.getAxolotlService().purgeKey(fingerprint);
 878						refreshUi();
 879					}
 880				});
 881		builder.create().show();
 882	}
 883
 884	public boolean hasStoragePermission(int requestCode) {
 885		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 886			if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
 887				requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
 888				return false;
 889			} else {
 890				return true;
 891			}
 892		} else {
 893			return true;
 894		}
 895	}
 896
 897	public void selectPresence(final Conversation conversation,
 898			final OnPresenceSelected listener) {
 899		final Contact contact = conversation.getContact();
 900		if (conversation.hasValidOtrSession()) {
 901			SessionID id = conversation.getOtrSession().getSessionID();
 902			Jid jid;
 903			try {
 904				jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID());
 905			} catch (InvalidJidException e) {
 906				jid = null;
 907			}
 908			conversation.setNextCounterpart(jid);
 909			listener.onPresenceSelected();
 910		} else 	if (!contact.showInRoster()) {
 911			showAddToRosterDialog(conversation);
 912		} else {
 913			Presences presences = contact.getPresences();
 914			if (presences.size() == 0) {
 915				if (!contact.getOption(Contact.Options.TO)
 916						&& !contact.getOption(Contact.Options.ASKING)
 917						&& contact.getAccount().getStatus() == Account.State.ONLINE) {
 918					showAskForPresenceDialog(contact);
 919				} else if (!contact.getOption(Contact.Options.TO)
 920						|| !contact.getOption(Contact.Options.FROM)) {
 921					warnMutalPresenceSubscription(conversation, listener);
 922				} else {
 923					conversation.setNextCounterpart(null);
 924					listener.onPresenceSelected();
 925				}
 926			} else if (presences.size() == 1) {
 927				String presence = presences.asStringArray()[0];
 928				try {
 929					conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence));
 930				} catch (InvalidJidException e) {
 931					conversation.setNextCounterpart(null);
 932				}
 933				listener.onPresenceSelected();
 934			} else {
 935				final StringBuilder presence = new StringBuilder();
 936				AlertDialog.Builder builder = new AlertDialog.Builder(this);
 937				builder.setTitle(getString(R.string.choose_presence));
 938				final String[] presencesArray = presences.asStringArray();
 939				int preselectedPresence = 0;
 940				for (int i = 0; i < presencesArray.length; ++i) {
 941					if (presencesArray[i].equals(contact.getLastPresence())) {
 942						preselectedPresence = i;
 943						break;
 944					}
 945				}
 946				presence.append(presencesArray[preselectedPresence]);
 947				builder.setSingleChoiceItems(presencesArray,
 948						preselectedPresence,
 949						new DialogInterface.OnClickListener() {
 950
 951							@Override
 952							public void onClick(DialogInterface dialog,
 953									int which) {
 954								presence.delete(0, presence.length());
 955								presence.append(presencesArray[which]);
 956							}
 957						});
 958				builder.setNegativeButton(R.string.cancel, null);
 959				builder.setPositiveButton(R.string.ok, new OnClickListener() {
 960
 961					@Override
 962					public void onClick(DialogInterface dialog, int which) {
 963						try {
 964							conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString()));
 965						} catch (InvalidJidException e) {
 966							conversation.setNextCounterpart(null);
 967						}
 968						listener.onPresenceSelected();
 969					}
 970				});
 971				builder.create().show();
 972			}
 973		}
 974	}
 975
 976	protected void onActivityResult(int requestCode, int resultCode,
 977			final Intent data) {
 978		super.onActivityResult(requestCode, resultCode, data);
 979		if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
 980			mPendingConferenceInvite = ConferenceInvite.parse(data);
 981			if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
 982				mPendingConferenceInvite.execute(this);
 983				mToast = Toast.makeText(this, R.string.creating_conference,Toast.LENGTH_LONG);
 984				mToast.show();
 985				mPendingConferenceInvite = null;
 986			}
 987		}
 988	}
 989
 990
 991	private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
 992		@Override
 993		public void success(final Conversation conversation) {
 994			runOnUiThread(new Runnable() {
 995				@Override
 996				public void run() {
 997					switchToConversation(conversation);
 998					hideToast();
 999				}
1000			});
1001		}
1002
1003		@Override
1004		public void error(final int errorCode, Conversation object) {
1005			runOnUiThread(new Runnable() {
1006				@Override
1007				public void run() {
1008					replaceToast(getString(errorCode));
1009				}
1010			});
1011		}
1012
1013		@Override
1014		public void userInputRequried(PendingIntent pi, Conversation object) {
1015
1016		}
1017	};
1018
1019	public int getTertiaryTextColor() {
1020		return this.mTertiaryTextColor;
1021	}
1022
1023	public int getSecondaryTextColor() {
1024		return this.mSecondaryTextColor;
1025	}
1026
1027	public int getPrimaryTextColor() {
1028		return this.mPrimaryTextColor;
1029	}
1030
1031	public int getWarningTextColor() {
1032		return this.mColorRed;
1033	}
1034
1035	public int getOnlineColor() {
1036		return this.mColorGreen;
1037	}
1038
1039	public int getPrimaryBackgroundColor() {
1040		return this.mPrimaryBackgroundColor;
1041	}
1042
1043	public int getSecondaryBackgroundColor() {
1044		return this.mSecondaryBackgroundColor;
1045	}
1046
1047	public int getPixel(int dp) {
1048		DisplayMetrics metrics = getResources().getDisplayMetrics();
1049		return ((int) (dp * metrics.density));
1050	}
1051
1052	public boolean copyTextToClipboard(String text, int labelResId) {
1053		ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
1054		String label = getResources().getString(labelResId);
1055		if (mClipBoardManager != null) {
1056			ClipData mClipData = ClipData.newPlainText(label, text);
1057			mClipBoardManager.setPrimaryClip(mClipData);
1058			return true;
1059		}
1060		return false;
1061	}
1062
1063	protected void registerNdefPushMessageCallback() {
1064		NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
1065		if (nfcAdapter != null && nfcAdapter.isEnabled()) {
1066			nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
1067				@Override
1068				public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
1069					return new NdefMessage(new NdefRecord[]{
1070							NdefRecord.createUri(getShareableUri()),
1071							NdefRecord.createApplicationRecord("eu.siacs.conversations")
1072					});
1073				}
1074			}, this);
1075		}
1076	}
1077
1078	protected boolean neverCompressPictures() {
1079		return getPreferences().getString("picture_compression", "auto").equals("never");
1080	}
1081
1082	protected boolean manuallyChangePresence() {
1083		return getPreferences().getBoolean("manually_change_presence", false);
1084	}
1085
1086	protected void unregisterNdefPushMessageCallback() {
1087		NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
1088		if (nfcAdapter != null && nfcAdapter.isEnabled()) {
1089			nfcAdapter.setNdefPushMessageCallback(null,this);
1090		}
1091	}
1092
1093	protected String getShareableUri() {
1094		return null;
1095	}
1096
1097	@Override
1098	public void onResume() {
1099		super.onResume();
1100		if (this.getShareableUri()!=null) {
1101			this.registerNdefPushMessageCallback();
1102		}
1103	}
1104
1105	protected int findTheme() {
1106		Boolean dark   = getPreferences().getString("theme", "light").equals("dark");
1107		Boolean larger = getPreferences().getBoolean("use_larger_font", false);
1108
1109		if(dark) {
1110			if(larger)
1111				return R.style.ConversationsTheme_Dark_LargerText;
1112			else
1113				return R.style.ConversationsTheme_Dark;
1114		} else {
1115			if (larger)
1116				return R.style.ConversationsTheme_LargerText;
1117			else
1118				return R.style.ConversationsTheme;
1119		}
1120	}
1121
1122	@Override
1123	public void onPause() {
1124		super.onPause();
1125		this.unregisterNdefPushMessageCallback();
1126	}
1127
1128	protected void showQrCode() {
1129		String uri = getShareableUri();
1130		if (uri!=null) {
1131			Point size = new Point();
1132			getWindowManager().getDefaultDisplay().getSize(size);
1133			final int width = (size.x < size.y ? size.x : size.y);
1134			Bitmap bitmap = createQrCodeBitmap(uri, width);
1135			ImageView view = new ImageView(this);
1136			view.setBackgroundColor(Color.WHITE);
1137			view.setImageBitmap(bitmap);
1138			AlertDialog.Builder builder = new AlertDialog.Builder(this);
1139			builder.setView(view);
1140			builder.create().show();
1141		}
1142	}
1143
1144	protected Bitmap createQrCodeBitmap(String input, int size) {
1145		Log.d(Config.LOGTAG,"qr code requested size: "+size);
1146		try {
1147			final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
1148			final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
1149			hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
1150			final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
1151			final int width = result.getWidth();
1152			final int height = result.getHeight();
1153			final int[] pixels = new int[width * height];
1154			for (int y = 0; y < height; y++) {
1155				final int offset = y * width;
1156				for (int x = 0; x < width; x++) {
1157					pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
1158				}
1159			}
1160			final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1161			Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
1162			bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
1163			return bitmap;
1164		} catch (final WriterException e) {
1165			return null;
1166		}
1167	}
1168
1169	protected Account extractAccount(Intent intent) {
1170		String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
1171		try {
1172			return jid != null ? xmppConnectionService.findAccountByJid(Jid.fromString(jid)) : null;
1173		} catch (InvalidJidException e) {
1174			return null;
1175		}
1176	}
1177
1178	public static class ConferenceInvite {
1179		private String uuid;
1180		private List<Jid> jids = new ArrayList<>();
1181
1182		public static ConferenceInvite parse(Intent data) {
1183			ConferenceInvite invite = new ConferenceInvite();
1184			invite.uuid = data.getStringExtra("conversation");
1185			if (invite.uuid == null) {
1186				return null;
1187			}
1188			try {
1189				if (data.getBooleanExtra("multiple", false)) {
1190					String[] toAdd = data.getStringArrayExtra("contacts");
1191					for (String item : toAdd) {
1192						invite.jids.add(Jid.fromString(item));
1193					}
1194				} else {
1195					invite.jids.add(Jid.fromString(data.getStringExtra("contact")));
1196				}
1197			} catch (final InvalidJidException ignored) {
1198				return null;
1199			}
1200			return invite;
1201		}
1202
1203		public void execute(XmppActivity activity) {
1204			XmppConnectionService service = activity.xmppConnectionService;
1205			Conversation conversation = service.findConversationByUuid(this.uuid);
1206			if (conversation == null) {
1207				return;
1208			}
1209			if (conversation.getMode() == Conversation.MODE_MULTI) {
1210				for (Jid jid : jids) {
1211					service.invite(conversation, jid);
1212				}
1213			} else {
1214				jids.add(conversation.getJid().toBareJid());
1215				service.createAdhocConference(conversation.getAccount(), null, jids, activity.adhocCallback);
1216			}
1217		}
1218	}
1219
1220	public AvatarService avatarService() {
1221		return xmppConnectionService.getAvatarService();
1222	}
1223
1224	class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
1225		private final WeakReference<ImageView> imageViewReference;
1226		private Message message = null;
1227
1228		public BitmapWorkerTask(ImageView imageView) {
1229			imageViewReference = new WeakReference<>(imageView);
1230		}
1231
1232		@Override
1233		protected Bitmap doInBackground(Message... params) {
1234			if (isCancelled()) {
1235				return null;
1236			}
1237			message = params[0];
1238			try {
1239				return xmppConnectionService.getFileBackend().getThumbnail(
1240						message, (int) (metrics.density * 288), false);
1241			} catch (FileNotFoundException e) {
1242				return null;
1243			}
1244		}
1245
1246		@Override
1247		protected void onPostExecute(Bitmap bitmap) {
1248			if (bitmap != null && !isCancelled()) {
1249				final ImageView imageView = imageViewReference.get();
1250				if (imageView != null) {
1251					imageView.setImageBitmap(bitmap);
1252					imageView.setBackgroundColor(0x00000000);
1253				}
1254			}
1255		}
1256	}
1257
1258	public void loadBitmap(Message message, ImageView imageView) {
1259		Bitmap bm;
1260		try {
1261			bm = xmppConnectionService.getFileBackend().getThumbnail(message,
1262					(int) (metrics.density * 288), true);
1263		} catch (FileNotFoundException e) {
1264			bm = null;
1265		}
1266		if (bm != null) {
1267			cancelPotentialWork(message, imageView);
1268			imageView.setImageBitmap(bm);
1269			imageView.setBackgroundColor(0x00000000);
1270		} else {
1271			if (cancelPotentialWork(message, imageView)) {
1272				imageView.setBackgroundColor(0xff333333);
1273				imageView.setImageDrawable(null);
1274				final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1275				final AsyncDrawable asyncDrawable = new AsyncDrawable(
1276						getResources(), null, task);
1277				imageView.setImageDrawable(asyncDrawable);
1278				try {
1279					task.execute(message);
1280				} catch (final RejectedExecutionException ignored) {
1281					ignored.printStackTrace();
1282				}
1283			}
1284		}
1285	}
1286
1287	public static boolean cancelPotentialWork(Message message, ImageView imageView) {
1288		final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
1289
1290		if (bitmapWorkerTask != null) {
1291			final Message oldMessage = bitmapWorkerTask.message;
1292			if (oldMessage == null || message != oldMessage) {
1293				bitmapWorkerTask.cancel(true);
1294			} else {
1295				return false;
1296			}
1297		}
1298		return true;
1299	}
1300
1301	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
1302		if (imageView != null) {
1303			final Drawable drawable = imageView.getDrawable();
1304			if (drawable instanceof AsyncDrawable) {
1305				final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
1306				return asyncDrawable.getBitmapWorkerTask();
1307			}
1308		}
1309		return null;
1310	}
1311
1312	static class AsyncDrawable extends BitmapDrawable {
1313		private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1314
1315		public AsyncDrawable(Resources res, Bitmap bitmap,
1316				BitmapWorkerTask bitmapWorkerTask) {
1317			super(res, bitmap);
1318			bitmapWorkerTaskReference = new WeakReference<>(
1319					bitmapWorkerTask);
1320		}
1321
1322		public BitmapWorkerTask getBitmapWorkerTask() {
1323			return bitmapWorkerTaskReference.get();
1324		}
1325	}
1326}