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		intent.putExtra("show_enter_jid", true);
 473		intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString());
 474		startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION);
 475	}
 476
 477	protected void announcePgp(Account account, final Conversation conversation) {
 478		if (account.getPgpId() == -1) {
 479			choosePgpSignId(account);
 480		} else {
 481			xmppConnectionService.getPgpEngine().generateSignature(account, "", new UiCallback<Account>() {
 482
 483				@Override
 484				public void userInputRequried(PendingIntent pi,
 485											  Account account) {
 486					try {
 487						startIntentSenderForResult(pi.getIntentSender(),
 488								REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
 489					} catch (final SendIntentException ignored) {
 490					}
 491				}
 492
 493				@Override
 494				public void success(Account account) {
 495					xmppConnectionService.databaseBackend.updateAccount(account);
 496					xmppConnectionService.sendPresence(account);
 497					if (conversation != null) {
 498						conversation.setNextEncryption(Message.ENCRYPTION_PGP);
 499						xmppConnectionService.databaseBackend.updateConversation(conversation);
 500					}
 501				}
 502
 503				@Override
 504				public void error(int error, Account account) {
 505					displayErrorDialog(error);
 506				}
 507			});
 508		}
 509	}
 510
 511	protected void choosePgpSignId(Account account) {
 512		xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<Account>() {
 513			@Override
 514			public void success(Account account1) {
 515			}
 516
 517			@Override
 518			public void error(int errorCode, Account object) {
 519
 520			}
 521
 522			@Override
 523			public void userInputRequried(PendingIntent pi, Account object) {
 524				try {
 525					startIntentSenderForResult(pi.getIntentSender(),
 526							REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0);
 527				} catch (final SendIntentException ignored) {
 528				}
 529			}
 530		});
 531	}
 532
 533	protected void displayErrorDialog(final int errorCode) {
 534		runOnUiThread(new Runnable() {
 535
 536			@Override
 537			public void run() {
 538				AlertDialog.Builder builder = new AlertDialog.Builder(
 539						XmppActivity.this);
 540				builder.setIconAttribute(android.R.attr.alertDialogIcon);
 541				builder.setTitle(getString(R.string.error));
 542				builder.setMessage(errorCode);
 543				builder.setNeutralButton(R.string.accept, null);
 544				builder.create().show();
 545			}
 546		});
 547
 548	}
 549
 550	protected void showAddToRosterDialog(final Conversation conversation) {
 551		showAddToRosterDialog(conversation.getContact());
 552	}
 553
 554	protected void showAddToRosterDialog(final Contact contact) {
 555		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 556		builder.setTitle(contact.getJid().toString());
 557		builder.setMessage(getString(R.string.not_in_roster));
 558		builder.setNegativeButton(getString(R.string.cancel), null);
 559		builder.setPositiveButton(getString(R.string.add_contact),
 560				new DialogInterface.OnClickListener() {
 561
 562					@Override
 563					public void onClick(DialogInterface dialog, int which) {
 564						final Jid jid = contact.getJid();
 565						Account account = contact.getAccount();
 566						Contact contact = account.getRoster().getContact(jid);
 567						xmppConnectionService.createContact(contact);
 568					}
 569				});
 570		builder.create().show();
 571	}
 572
 573	private void showAskForPresenceDialog(final Contact contact) {
 574		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 575		builder.setTitle(contact.getJid().toString());
 576		builder.setMessage(R.string.request_presence_updates);
 577		builder.setNegativeButton(R.string.cancel, null);
 578		builder.setPositiveButton(R.string.request_now,
 579				new DialogInterface.OnClickListener() {
 580
 581					@Override
 582					public void onClick(DialogInterface dialog, int which) {
 583						if (xmppConnectionServiceBound) {
 584							xmppConnectionService.sendPresencePacket(contact
 585									.getAccount(), xmppConnectionService
 586									.getPresenceGenerator()
 587									.requestPresenceUpdatesFrom(contact));
 588						}
 589					}
 590				});
 591		builder.create().show();
 592	}
 593
 594	private void warnMutalPresenceSubscription(final Conversation conversation,
 595			final OnPresenceSelected listener) {
 596		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 597		builder.setTitle(conversation.getContact().getJid().toString());
 598		builder.setMessage(R.string.without_mutual_presence_updates);
 599		builder.setNegativeButton(R.string.cancel, null);
 600		builder.setPositiveButton(R.string.ignore, new OnClickListener() {
 601
 602			@Override
 603			public void onClick(DialogInterface dialog, int which) {
 604				conversation.setNextCounterpart(null);
 605				if (listener != null) {
 606					listener.onPresenceSelected();
 607				}
 608			}
 609		});
 610		builder.create().show();
 611	}
 612
 613	protected void quickEdit(String previousValue, OnValueEdited callback) {
 614		quickEdit(previousValue, callback, false);
 615	}
 616
 617	protected void quickPasswordEdit(String previousValue,
 618			OnValueEdited callback) {
 619		quickEdit(previousValue, callback, true);
 620	}
 621
 622	@SuppressLint("InflateParams")
 623	private void quickEdit(final String previousValue,
 624			final OnValueEdited callback, boolean password) {
 625		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 626		View view = getLayoutInflater().inflate(R.layout.quickedit, null);
 627		final EditText editor = (EditText) view.findViewById(R.id.editor);
 628		OnClickListener mClickListener = new OnClickListener() {
 629
 630			@Override
 631			public void onClick(DialogInterface dialog, int which) {
 632				String value = editor.getText().toString();
 633				if (!previousValue.equals(value) && value.trim().length() > 0) {
 634					callback.onValueEdited(value);
 635				}
 636			}
 637		};
 638		if (password) {
 639			editor.setInputType(InputType.TYPE_CLASS_TEXT
 640					| InputType.TYPE_TEXT_VARIATION_PASSWORD);
 641			editor.setHint(R.string.password);
 642			builder.setPositiveButton(R.string.accept, mClickListener);
 643		} else {
 644			builder.setPositiveButton(R.string.edit, mClickListener);
 645		}
 646		editor.requestFocus();
 647		editor.setText(previousValue);
 648		builder.setView(view);
 649		builder.setNegativeButton(R.string.cancel, null);
 650		builder.create().show();
 651	}
 652
 653	protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight, View.OnClickListener onKeyClickedListener) {
 654		final XmppAxolotlSession.Trust trust = account.getAxolotlService()
 655				.getFingerprintTrust(fingerprint);
 656		if (trust == null) {
 657			return false;
 658		}
 659		return addFingerprintRowWithListeners(keys, account, fingerprint, highlight, trust, true,
 660				new CompoundButton.OnCheckedChangeListener() {
 661					@Override
 662					public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
 663						account.getAxolotlService().setFingerprintTrust(fingerprint,
 664								(isChecked) ? XmppAxolotlSession.Trust.TRUSTED :
 665										XmppAxolotlSession.Trust.UNTRUSTED);
 666					}
 667				},
 668				new View.OnClickListener() {
 669					@Override
 670					public void onClick(View v) {
 671						account.getAxolotlService().setFingerprintTrust(fingerprint,
 672								XmppAxolotlSession.Trust.UNTRUSTED);
 673						v.setEnabled(true);
 674					}
 675				},
 676				onKeyClickedListener
 677
 678		);
 679	}
 680
 681	protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account,
 682	                                                 final String fingerprint,
 683	                                                 boolean highlight,
 684	                                                 XmppAxolotlSession.Trust trust,
 685	                                                 boolean showTag,
 686	                                                 CompoundButton.OnCheckedChangeListener
 687			                                                 onCheckedChangeListener,
 688	                                                 View.OnClickListener onClickListener,
 689													 View.OnClickListener onKeyClickedListener) {
 690		if (trust == XmppAxolotlSession.Trust.COMPROMISED) {
 691			return false;
 692		}
 693		View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
 694		TextView key = (TextView) view.findViewById(R.id.key);
 695		key.setOnClickListener(onKeyClickedListener);
 696		TextView keyType = (TextView) view.findViewById(R.id.key_type);
 697		keyType.setOnClickListener(onKeyClickedListener);
 698		Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
 699		trustToggle.setVisibility(View.VISIBLE);
 700		trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
 701		trustToggle.setOnClickListener(onClickListener);
 702		final View.OnLongClickListener purge = new View.OnLongClickListener() {
 703			@Override
 704			public boolean onLongClick(View v) {
 705				showPurgeKeyDialog(account, fingerprint);
 706				return true;
 707			}
 708		};
 709		view.setOnLongClickListener(purge);
 710		key.setOnLongClickListener(purge);
 711		keyType.setOnLongClickListener(purge);
 712		boolean x509 = trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509;
 713		switch (trust) {
 714			case UNTRUSTED:
 715			case TRUSTED:
 716			case TRUSTED_X509:
 717				trustToggle.setChecked(trust.trusted(), false);
 718				trustToggle.setEnabled(trust != XmppAxolotlSession.Trust.TRUSTED_X509);
 719				if (trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
 720					trustToggle.setOnClickListener(null);
 721				}
 722				key.setTextColor(getPrimaryTextColor());
 723				keyType.setTextColor(getSecondaryTextColor());
 724				break;
 725			case UNDECIDED:
 726				trustToggle.setChecked(false, false);
 727				trustToggle.setEnabled(false);
 728				key.setTextColor(getPrimaryTextColor());
 729				keyType.setTextColor(getSecondaryTextColor());
 730				break;
 731			case INACTIVE_UNTRUSTED:
 732			case INACTIVE_UNDECIDED:
 733				trustToggle.setOnClickListener(null);
 734				trustToggle.setChecked(false, false);
 735				trustToggle.setEnabled(false);
 736				key.setTextColor(getTertiaryTextColor());
 737				keyType.setTextColor(getTertiaryTextColor());
 738				break;
 739			case INACTIVE_TRUSTED:
 740			case INACTIVE_TRUSTED_X509:
 741				trustToggle.setOnClickListener(null);
 742				trustToggle.setChecked(true, false);
 743				trustToggle.setEnabled(false);
 744				key.setTextColor(getTertiaryTextColor());
 745				keyType.setTextColor(getTertiaryTextColor());
 746				break;
 747		}
 748
 749		if (showTag) {
 750			keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
 751		} else {
 752			keyType.setVisibility(View.GONE);
 753		}
 754		if (highlight) {
 755			keyType.setTextColor(getResources().getColor(R.color.accent));
 756			keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
 757		} else {
 758			keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
 759		}
 760
 761		key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
 762		keys.addView(view);
 763		return true;
 764	}
 765
 766	public void showPurgeKeyDialog(final Account account, final String fingerprint) {
 767		Builder builder = new Builder(this);
 768		builder.setTitle(getString(R.string.purge_key));
 769		builder.setIconAttribute(android.R.attr.alertDialogIcon);
 770		builder.setMessage(getString(R.string.purge_key_desc_part1)
 771				+ "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2))
 772				+ "\n\n" + getString(R.string.purge_key_desc_part2));
 773		builder.setNegativeButton(getString(R.string.cancel), null);
 774		builder.setPositiveButton(getString(R.string.accept),
 775				new DialogInterface.OnClickListener() {
 776					@Override
 777					public void onClick(DialogInterface dialog, int which) {
 778						account.getAxolotlService().purgeKey(fingerprint);
 779						refreshUi();
 780					}
 781				});
 782		builder.create().show();
 783	}
 784
 785	public void selectPresence(final Conversation conversation,
 786			final OnPresenceSelected listener) {
 787		final Contact contact = conversation.getContact();
 788		if (conversation.hasValidOtrSession()) {
 789			SessionID id = conversation.getOtrSession().getSessionID();
 790			Jid jid;
 791			try {
 792				jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID());
 793			} catch (InvalidJidException e) {
 794				jid = null;
 795			}
 796			conversation.setNextCounterpart(jid);
 797			listener.onPresenceSelected();
 798		} else 	if (!contact.showInRoster()) {
 799			showAddToRosterDialog(conversation);
 800		} else {
 801			Presences presences = contact.getPresences();
 802			if (presences.size() == 0) {
 803				if (!contact.getOption(Contact.Options.TO)
 804						&& !contact.getOption(Contact.Options.ASKING)
 805						&& contact.getAccount().getStatus() == Account.State.ONLINE) {
 806					showAskForPresenceDialog(contact);
 807				} else if (!contact.getOption(Contact.Options.TO)
 808						|| !contact.getOption(Contact.Options.FROM)) {
 809					warnMutalPresenceSubscription(conversation, listener);
 810				} else {
 811					conversation.setNextCounterpart(null);
 812					listener.onPresenceSelected();
 813				}
 814			} else if (presences.size() == 1) {
 815				String presence = presences.asStringArray()[0];
 816				try {
 817					conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence));
 818				} catch (InvalidJidException e) {
 819					conversation.setNextCounterpart(null);
 820				}
 821				listener.onPresenceSelected();
 822			} else {
 823				final StringBuilder presence = new StringBuilder();
 824				AlertDialog.Builder builder = new AlertDialog.Builder(this);
 825				builder.setTitle(getString(R.string.choose_presence));
 826				final String[] presencesArray = presences.asStringArray();
 827				int preselectedPresence = 0;
 828				for (int i = 0; i < presencesArray.length; ++i) {
 829					if (presencesArray[i].equals(contact.lastseen.presence)) {
 830						preselectedPresence = i;
 831						break;
 832					}
 833				}
 834				presence.append(presencesArray[preselectedPresence]);
 835				builder.setSingleChoiceItems(presencesArray,
 836						preselectedPresence,
 837						new DialogInterface.OnClickListener() {
 838
 839							@Override
 840							public void onClick(DialogInterface dialog,
 841									int which) {
 842								presence.delete(0, presence.length());
 843								presence.append(presencesArray[which]);
 844							}
 845						});
 846				builder.setNegativeButton(R.string.cancel, null);
 847				builder.setPositiveButton(R.string.ok, new OnClickListener() {
 848
 849					@Override
 850					public void onClick(DialogInterface dialog, int which) {
 851						try {
 852							conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString()));
 853						} catch (InvalidJidException e) {
 854							conversation.setNextCounterpart(null);
 855						}
 856						listener.onPresenceSelected();
 857					}
 858				});
 859				builder.create().show();
 860			}
 861		}
 862	}
 863
 864	protected void onActivityResult(int requestCode, int resultCode,
 865			final Intent data) {
 866		super.onActivityResult(requestCode, resultCode, data);
 867		if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
 868			mPendingConferenceInvite = ConferenceInvite.parse(data);
 869			if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
 870				mPendingConferenceInvite.execute(this);
 871				mPendingConferenceInvite = null;
 872			}
 873		}
 874	}
 875
 876	private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
 877		@Override
 878		public void success(final Conversation conversation) {
 879			switchToConversation(conversation);
 880			runOnUiThread(new Runnable() {
 881				@Override
 882				public void run() {
 883					Toast.makeText(XmppActivity.this,R.string.conference_created,Toast.LENGTH_LONG).show();
 884				}
 885			});
 886		}
 887
 888		@Override
 889		public void error(final int errorCode, Conversation object) {
 890			runOnUiThread(new Runnable() {
 891				@Override
 892				public void run() {
 893					Toast.makeText(XmppActivity.this,errorCode,Toast.LENGTH_LONG).show();
 894				}
 895			});
 896		}
 897
 898		@Override
 899		public void userInputRequried(PendingIntent pi, Conversation object) {
 900
 901		}
 902	};
 903
 904	public int getTertiaryTextColor() {
 905		return this.mTertiaryTextColor;
 906	}
 907
 908	public int getSecondaryTextColor() {
 909		return this.mSecondaryTextColor;
 910	}
 911
 912	public int getPrimaryTextColor() {
 913		return this.mPrimaryTextColor;
 914	}
 915
 916	public int getWarningTextColor() {
 917		return this.mColorRed;
 918	}
 919
 920	public int getOnlineColor() {
 921		return this.mColorGreen;
 922	}
 923
 924	public int getPrimaryBackgroundColor() {
 925		return this.mPrimaryBackgroundColor;
 926	}
 927
 928	public int getSecondaryBackgroundColor() {
 929		return this.mSecondaryBackgroundColor;
 930	}
 931
 932	public int getPixel(int dp) {
 933		DisplayMetrics metrics = getResources().getDisplayMetrics();
 934		return ((int) (dp * metrics.density));
 935	}
 936
 937	public boolean copyTextToClipboard(String text, int labelResId) {
 938		ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
 939		String label = getResources().getString(labelResId);
 940		if (mClipBoardManager != null) {
 941			ClipData mClipData = ClipData.newPlainText(label, text);
 942			mClipBoardManager.setPrimaryClip(mClipData);
 943			return true;
 944		}
 945		return false;
 946	}
 947
 948	protected void registerNdefPushMessageCallback() {
 949		NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
 950		if (nfcAdapter != null && nfcAdapter.isEnabled()) {
 951			nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
 952				@Override
 953				public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
 954					return new NdefMessage(new NdefRecord[]{
 955						NdefRecord.createUri(getShareableUri()),
 956							NdefRecord.createApplicationRecord("eu.siacs.conversations")
 957					});
 958				}
 959			}, this);
 960		}
 961	}
 962
 963	protected void unregisterNdefPushMessageCallback() {
 964		NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
 965		if (nfcAdapter != null && nfcAdapter.isEnabled()) {
 966			nfcAdapter.setNdefPushMessageCallback(null,this);
 967		}
 968	}
 969
 970	protected String getShareableUri() {
 971		return null;
 972	}
 973
 974	@Override
 975	public void onResume() {
 976		super.onResume();
 977		if (this.getShareableUri()!=null) {
 978			this.registerNdefPushMessageCallback();
 979		}
 980	}
 981
 982	protected int findTheme() {
 983		if (getPreferences().getBoolean("use_larger_font", false)) {
 984			return R.style.ConversationsTheme_LargerText;
 985		} else {
 986			return R.style.ConversationsTheme;
 987		}
 988	}
 989
 990	@Override
 991	public void onPause() {
 992		super.onPause();
 993		this.unregisterNdefPushMessageCallback();
 994	}
 995
 996	protected void showQrCode() {
 997		String uri = getShareableUri();
 998		if (uri!=null) {
 999			Point size = new Point();
1000			getWindowManager().getDefaultDisplay().getSize(size);
1001			final int width = (size.x < size.y ? size.x : size.y);
1002			Bitmap bitmap = createQrCodeBitmap(uri, width);
1003			ImageView view = new ImageView(this);
1004			view.setImageBitmap(bitmap);
1005			AlertDialog.Builder builder = new AlertDialog.Builder(this);
1006			builder.setView(view);
1007			builder.create().show();
1008		}
1009	}
1010
1011	protected Bitmap createQrCodeBitmap(String input, int size) {
1012		Log.d(Config.LOGTAG,"qr code requested size: "+size);
1013		try {
1014			final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
1015			final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
1016			hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
1017			final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
1018			final int width = result.getWidth();
1019			final int height = result.getHeight();
1020			final int[] pixels = new int[width * height];
1021			for (int y = 0; y < height; y++) {
1022				final int offset = y * width;
1023				for (int x = 0; x < width; x++) {
1024					pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
1025				}
1026			}
1027			final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1028			Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
1029			bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
1030			return bitmap;
1031		} catch (final WriterException e) {
1032			return null;
1033		}
1034	}
1035
1036	public static class ConferenceInvite {
1037		private String uuid;
1038		private List<Jid> jids = new ArrayList<>();
1039
1040		public static ConferenceInvite parse(Intent data) {
1041			ConferenceInvite invite = new ConferenceInvite();
1042			invite.uuid = data.getStringExtra("conversation");
1043			if (invite.uuid == null) {
1044				return null;
1045			}
1046			try {
1047				if (data.getBooleanExtra("multiple", false)) {
1048					String[] toAdd = data.getStringArrayExtra("contacts");
1049					for (String item : toAdd) {
1050						invite.jids.add(Jid.fromString(item));
1051					}
1052				} else {
1053					invite.jids.add(Jid.fromString(data.getStringExtra("contact")));
1054				}
1055			} catch (final InvalidJidException ignored) {
1056				return null;
1057			}
1058			return invite;
1059		}
1060
1061		public void execute(XmppActivity activity) {
1062			XmppConnectionService service = activity.xmppConnectionService;
1063			Conversation conversation = service.findConversationByUuid(this.uuid);
1064			if (conversation == null) {
1065				return;
1066			}
1067			if (conversation.getMode() == Conversation.MODE_MULTI) {
1068				for (Jid jid : jids) {
1069					service.invite(conversation, jid);
1070				}
1071			} else {
1072				jids.add(conversation.getJid().toBareJid());
1073				service.createAdhocConference(conversation.getAccount(), jids, activity.adhocCallback);
1074			}
1075		}
1076	}
1077
1078	public AvatarService avatarService() {
1079		return xmppConnectionService.getAvatarService();
1080	}
1081
1082	class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
1083		private final WeakReference<ImageView> imageViewReference;
1084		private Message message = null;
1085
1086		public BitmapWorkerTask(ImageView imageView) {
1087			imageViewReference = new WeakReference<>(imageView);
1088		}
1089
1090		@Override
1091		protected Bitmap doInBackground(Message... params) {
1092			message = params[0];
1093			try {
1094				return xmppConnectionService.getFileBackend().getThumbnail(
1095						message, (int) (metrics.density * 288), false);
1096			} catch (FileNotFoundException e) {
1097				return null;
1098			}
1099		}
1100
1101		@Override
1102		protected void onPostExecute(Bitmap bitmap) {
1103			if (bitmap != null) {
1104				final ImageView imageView = imageViewReference.get();
1105				if (imageView != null) {
1106					imageView.setImageBitmap(bitmap);
1107					imageView.setBackgroundColor(0x00000000);
1108				}
1109			}
1110		}
1111	}
1112
1113	public void loadBitmap(Message message, ImageView imageView) {
1114		Bitmap bm;
1115		try {
1116			bm = xmppConnectionService.getFileBackend().getThumbnail(message,
1117					(int) (metrics.density * 288), true);
1118		} catch (FileNotFoundException e) {
1119			bm = null;
1120		}
1121		if (bm != null) {
1122			imageView.setImageBitmap(bm);
1123			imageView.setBackgroundColor(0x00000000);
1124		} else {
1125			if (cancelPotentialWork(message, imageView)) {
1126				imageView.setBackgroundColor(0xff333333);
1127				final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1128				final AsyncDrawable asyncDrawable = new AsyncDrawable(
1129						getResources(), null, task);
1130				imageView.setImageDrawable(asyncDrawable);
1131				try {
1132					task.execute(message);
1133				} catch (final RejectedExecutionException ignored) {
1134				}
1135			}
1136		}
1137	}
1138
1139	public static boolean cancelPotentialWork(Message message,
1140			ImageView imageView) {
1141		final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
1142
1143		if (bitmapWorkerTask != null) {
1144			final Message oldMessage = bitmapWorkerTask.message;
1145			if (oldMessage == null || message != oldMessage) {
1146				bitmapWorkerTask.cancel(true);
1147			} else {
1148				return false;
1149			}
1150		}
1151		return true;
1152	}
1153
1154	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
1155		if (imageView != null) {
1156			final Drawable drawable = imageView.getDrawable();
1157			if (drawable instanceof AsyncDrawable) {
1158				final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
1159				return asyncDrawable.getBitmapWorkerTask();
1160			}
1161		}
1162		return null;
1163	}
1164
1165	static class AsyncDrawable extends BitmapDrawable {
1166		private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1167
1168		public AsyncDrawable(Resources res, Bitmap bitmap,
1169				BitmapWorkerTask bitmapWorkerTask) {
1170			super(res, bitmap);
1171			bitmapWorkerTaskReference = new WeakReference<>(
1172					bitmapWorkerTask);
1173		}
1174
1175		public BitmapWorkerTask getBitmapWorkerTask() {
1176			return bitmapWorkerTaskReference.get();
1177		}
1178	}
1179}