XmppActivity.java

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