XmppActivity.java

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