XmppActivity.java

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