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				if (mPendingConferenceInvite.execute(this)) {
 985					mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
 986					mToast.show();
 987				}
 988				mPendingConferenceInvite = null;
 989			}
 990		}
 991	}
 992
 993
 994	private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
 995		@Override
 996		public void success(final Conversation conversation) {
 997			runOnUiThread(new Runnable() {
 998				@Override
 999				public void run() {
1000					switchToConversation(conversation);
1001					hideToast();
1002				}
1003			});
1004		}
1005
1006		@Override
1007		public void error(final int errorCode, Conversation object) {
1008			runOnUiThread(new Runnable() {
1009				@Override
1010				public void run() {
1011					replaceToast(getString(errorCode));
1012				}
1013			});
1014		}
1015
1016		@Override
1017		public void userInputRequried(PendingIntent pi, Conversation object) {
1018
1019		}
1020	};
1021
1022	public int getTertiaryTextColor() {
1023		return this.mTertiaryTextColor;
1024	}
1025
1026	public int getSecondaryTextColor() {
1027		return this.mSecondaryTextColor;
1028	}
1029
1030	public int getPrimaryTextColor() {
1031		return this.mPrimaryTextColor;
1032	}
1033
1034	public int getWarningTextColor() {
1035		return this.mColorRed;
1036	}
1037
1038	public int getOnlineColor() {
1039		return this.mColorGreen;
1040	}
1041
1042	public int getPrimaryBackgroundColor() {
1043		return this.mPrimaryBackgroundColor;
1044	}
1045
1046	public int getSecondaryBackgroundColor() {
1047		return this.mSecondaryBackgroundColor;
1048	}
1049
1050	public int getPixel(int dp) {
1051		DisplayMetrics metrics = getResources().getDisplayMetrics();
1052		return ((int) (dp * metrics.density));
1053	}
1054
1055	public boolean copyTextToClipboard(String text, int labelResId) {
1056		ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
1057		String label = getResources().getString(labelResId);
1058		if (mClipBoardManager != null) {
1059			ClipData mClipData = ClipData.newPlainText(label, text);
1060			mClipBoardManager.setPrimaryClip(mClipData);
1061			return true;
1062		}
1063		return false;
1064	}
1065
1066	protected void registerNdefPushMessageCallback() {
1067		NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
1068		if (nfcAdapter != null && nfcAdapter.isEnabled()) {
1069			nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
1070				@Override
1071				public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
1072					return new NdefMessage(new NdefRecord[]{
1073							NdefRecord.createUri(getShareableUri()),
1074							NdefRecord.createApplicationRecord("eu.siacs.conversations")
1075					});
1076				}
1077			}, this);
1078		}
1079	}
1080
1081	protected boolean neverCompressPictures() {
1082		return getPreferences().getString("picture_compression", "auto").equals("never");
1083	}
1084
1085	protected boolean manuallyChangePresence() {
1086		return getPreferences().getBoolean("manually_change_presence", false);
1087	}
1088
1089	protected void unregisterNdefPushMessageCallback() {
1090		NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
1091		if (nfcAdapter != null && nfcAdapter.isEnabled()) {
1092			nfcAdapter.setNdefPushMessageCallback(null,this);
1093		}
1094	}
1095
1096	protected String getShareableUri() {
1097		return null;
1098	}
1099
1100	protected void shareUri() {
1101		String uri = getShareableUri();
1102		if (uri == null || uri.isEmpty()) {
1103			return;
1104		}
1105		Intent shareIntent = new Intent();
1106		shareIntent.setAction(Intent.ACTION_SEND);
1107		shareIntent.putExtra(Intent.EXTRA_TEXT, getShareableUri());
1108		shareIntent.setType("text/plain");
1109		try {
1110			startActivity(Intent.createChooser(shareIntent, getText(R.string.share_uri_with)));
1111		} catch (ActivityNotFoundException e) {
1112			Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
1113		}
1114	}
1115
1116	@Override
1117	public void onResume() {
1118		super.onResume();
1119		if (this.getShareableUri()!=null) {
1120			this.registerNdefPushMessageCallback();
1121		}
1122	}
1123
1124	protected int findTheme() {
1125		Boolean dark   = getPreferences().getString("theme", "light").equals("dark");
1126		Boolean larger = getPreferences().getBoolean("use_larger_font", false);
1127
1128		if(dark) {
1129			if(larger)
1130				return R.style.ConversationsTheme_Dark_LargerText;
1131			else
1132				return R.style.ConversationsTheme_Dark;
1133		} else {
1134			if (larger)
1135				return R.style.ConversationsTheme_LargerText;
1136			else
1137				return R.style.ConversationsTheme;
1138		}
1139	}
1140
1141	@Override
1142	public void onPause() {
1143		super.onPause();
1144		this.unregisterNdefPushMessageCallback();
1145	}
1146
1147	protected void showQrCode() {
1148		String uri = getShareableUri();
1149		if (uri!=null) {
1150			Point size = new Point();
1151			getWindowManager().getDefaultDisplay().getSize(size);
1152			final int width = (size.x < size.y ? size.x : size.y);
1153			Bitmap bitmap = createQrCodeBitmap(uri, width);
1154			ImageView view = new ImageView(this);
1155			view.setBackgroundColor(Color.WHITE);
1156			view.setImageBitmap(bitmap);
1157			AlertDialog.Builder builder = new AlertDialog.Builder(this);
1158			builder.setView(view);
1159			builder.create().show();
1160		}
1161	}
1162
1163	protected Bitmap createQrCodeBitmap(String input, int size) {
1164		Log.d(Config.LOGTAG,"qr code requested size: "+size);
1165		try {
1166			final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
1167			final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
1168			hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
1169			final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
1170			final int width = result.getWidth();
1171			final int height = result.getHeight();
1172			final int[] pixels = new int[width * height];
1173			for (int y = 0; y < height; y++) {
1174				final int offset = y * width;
1175				for (int x = 0; x < width; x++) {
1176					pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
1177				}
1178			}
1179			final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1180			Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
1181			bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
1182			return bitmap;
1183		} catch (final WriterException e) {
1184			return null;
1185		}
1186	}
1187
1188	protected Account extractAccount(Intent intent) {
1189		String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
1190		try {
1191			return jid != null ? xmppConnectionService.findAccountByJid(Jid.fromString(jid)) : null;
1192		} catch (InvalidJidException e) {
1193			return null;
1194		}
1195	}
1196
1197	public static class ConferenceInvite {
1198		private String uuid;
1199		private List<Jid> jids = new ArrayList<>();
1200
1201		public static ConferenceInvite parse(Intent data) {
1202			ConferenceInvite invite = new ConferenceInvite();
1203			invite.uuid = data.getStringExtra("conversation");
1204			if (invite.uuid == null) {
1205				return null;
1206			}
1207			try {
1208				if (data.getBooleanExtra("multiple", false)) {
1209					String[] toAdd = data.getStringArrayExtra("contacts");
1210					for (String item : toAdd) {
1211						invite.jids.add(Jid.fromString(item));
1212					}
1213				} else {
1214					invite.jids.add(Jid.fromString(data.getStringExtra("contact")));
1215				}
1216			} catch (final InvalidJidException ignored) {
1217				return null;
1218			}
1219			return invite;
1220		}
1221
1222		public boolean execute(XmppActivity activity) {
1223			XmppConnectionService service = activity.xmppConnectionService;
1224			Conversation conversation = service.findConversationByUuid(this.uuid);
1225			if (conversation == null) {
1226				return false;
1227			}
1228			if (conversation.getMode() == Conversation.MODE_MULTI) {
1229				for (Jid jid : jids) {
1230					service.invite(conversation, jid);
1231				}
1232				return false;
1233			} else {
1234				jids.add(conversation.getJid().toBareJid());
1235				service.createAdhocConference(conversation.getAccount(), null, jids, activity.adhocCallback);
1236				return true;
1237			}
1238		}
1239	}
1240
1241	public AvatarService avatarService() {
1242		return xmppConnectionService.getAvatarService();
1243	}
1244
1245	class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
1246		private final WeakReference<ImageView> imageViewReference;
1247		private Message message = null;
1248
1249		public BitmapWorkerTask(ImageView imageView) {
1250			imageViewReference = new WeakReference<>(imageView);
1251		}
1252
1253		@Override
1254		protected Bitmap doInBackground(Message... params) {
1255			if (isCancelled()) {
1256				return null;
1257			}
1258			message = params[0];
1259			try {
1260				return xmppConnectionService.getFileBackend().getThumbnail(
1261						message, (int) (metrics.density * 288), false);
1262			} catch (FileNotFoundException e) {
1263				return null;
1264			}
1265		}
1266
1267		@Override
1268		protected void onPostExecute(Bitmap bitmap) {
1269			if (bitmap != null && !isCancelled()) {
1270				final ImageView imageView = imageViewReference.get();
1271				if (imageView != null) {
1272					imageView.setImageBitmap(bitmap);
1273					imageView.setBackgroundColor(0x00000000);
1274				}
1275			}
1276		}
1277	}
1278
1279	public void loadBitmap(Message message, ImageView imageView) {
1280		Bitmap bm;
1281		try {
1282			bm = xmppConnectionService.getFileBackend().getThumbnail(message,
1283					(int) (metrics.density * 288), true);
1284		} catch (FileNotFoundException e) {
1285			bm = null;
1286		}
1287		if (bm != null) {
1288			cancelPotentialWork(message, imageView);
1289			imageView.setImageBitmap(bm);
1290			imageView.setBackgroundColor(0x00000000);
1291		} else {
1292			if (cancelPotentialWork(message, imageView)) {
1293				imageView.setBackgroundColor(0xff333333);
1294				imageView.setImageDrawable(null);
1295				final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1296				final AsyncDrawable asyncDrawable = new AsyncDrawable(
1297						getResources(), null, task);
1298				imageView.setImageDrawable(asyncDrawable);
1299				try {
1300					task.execute(message);
1301				} catch (final RejectedExecutionException ignored) {
1302					ignored.printStackTrace();
1303				}
1304			}
1305		}
1306	}
1307
1308	public static boolean cancelPotentialWork(Message message, ImageView imageView) {
1309		final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
1310
1311		if (bitmapWorkerTask != null) {
1312			final Message oldMessage = bitmapWorkerTask.message;
1313			if (oldMessage == null || message != oldMessage) {
1314				bitmapWorkerTask.cancel(true);
1315			} else {
1316				return false;
1317			}
1318		}
1319		return true;
1320	}
1321
1322	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
1323		if (imageView != null) {
1324			final Drawable drawable = imageView.getDrawable();
1325			if (drawable instanceof AsyncDrawable) {
1326				final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
1327				return asyncDrawable.getBitmapWorkerTask();
1328			}
1329		}
1330		return null;
1331	}
1332
1333	static class AsyncDrawable extends BitmapDrawable {
1334		private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1335
1336		public AsyncDrawable(Resources res, Bitmap bitmap,
1337				BitmapWorkerTask bitmapWorkerTask) {
1338			super(res, bitmap);
1339			bitmapWorkerTaskReference = new WeakReference<>(
1340					bitmapWorkerTask);
1341		}
1342
1343		public BitmapWorkerTask getBitmapWorkerTask() {
1344			return bitmapWorkerTaskReference.get();
1345		}
1346	}
1347}