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		view.setOnLongClickListener(new View.OnLongClickListener() {
 701			@Override
 702			public boolean onLongClick(View v) {
 703				showPurgeKeyDialog(account, fingerprint);
 704				return true;
 705			}
 706		});
 707		boolean x509 = trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509;
 708		switch (trust) {
 709			case UNTRUSTED:
 710			case TRUSTED:
 711			case TRUSTED_X509:
 712				trustToggle.setChecked(trust.trusted(), false);
 713				trustToggle.setEnabled(trust != XmppAxolotlSession.Trust.TRUSTED_X509);
 714				if (trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
 715					trustToggle.setOnClickListener(null);
 716				}
 717				key.setTextColor(getPrimaryTextColor());
 718				keyType.setTextColor(getSecondaryTextColor());
 719				break;
 720			case UNDECIDED:
 721				trustToggle.setChecked(false, false);
 722				trustToggle.setEnabled(false);
 723				key.setTextColor(getPrimaryTextColor());
 724				keyType.setTextColor(getSecondaryTextColor());
 725				break;
 726			case INACTIVE_UNTRUSTED:
 727			case INACTIVE_UNDECIDED:
 728				trustToggle.setOnClickListener(null);
 729				trustToggle.setChecked(false, false);
 730				trustToggle.setEnabled(false);
 731				key.setTextColor(getTertiaryTextColor());
 732				keyType.setTextColor(getTertiaryTextColor());
 733				break;
 734			case INACTIVE_TRUSTED:
 735			case INACTIVE_TRUSTED_X509:
 736				trustToggle.setOnClickListener(null);
 737				trustToggle.setChecked(true, false);
 738				trustToggle.setEnabled(false);
 739				key.setTextColor(getTertiaryTextColor());
 740				keyType.setTextColor(getTertiaryTextColor());
 741				break;
 742		}
 743
 744		if (showTag) {
 745			keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
 746		} else {
 747			keyType.setVisibility(View.GONE);
 748		}
 749		if (highlight) {
 750			keyType.setTextColor(getResources().getColor(R.color.accent));
 751			keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
 752		} else {
 753			keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
 754		}
 755
 756		key.setText(CryptoHelper.prettifyFingerprint(fingerprint));
 757		keys.addView(view);
 758		return true;
 759	}
 760
 761	public void showPurgeKeyDialog(final Account account, final String fingerprint) {
 762		Builder builder = new Builder(this);
 763		builder.setTitle(getString(R.string.purge_key));
 764		builder.setIconAttribute(android.R.attr.alertDialogIcon);
 765		builder.setMessage(getString(R.string.purge_key_desc_part1)
 766				+ "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint)
 767				+ "\n\n" + getString(R.string.purge_key_desc_part2));
 768		builder.setNegativeButton(getString(R.string.cancel), null);
 769		builder.setPositiveButton(getString(R.string.accept),
 770				new DialogInterface.OnClickListener() {
 771					@Override
 772					public void onClick(DialogInterface dialog, int which) {
 773						account.getAxolotlService().purgeKey(fingerprint);
 774						refreshUi();
 775					}
 776				});
 777		builder.create().show();
 778	}
 779
 780	public void selectPresence(final Conversation conversation,
 781			final OnPresenceSelected listener) {
 782		final Contact contact = conversation.getContact();
 783		if (conversation.hasValidOtrSession()) {
 784			SessionID id = conversation.getOtrSession().getSessionID();
 785			Jid jid;
 786			try {
 787				jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID());
 788			} catch (InvalidJidException e) {
 789				jid = null;
 790			}
 791			conversation.setNextCounterpart(jid);
 792			listener.onPresenceSelected();
 793		} else 	if (!contact.showInRoster()) {
 794			showAddToRosterDialog(conversation);
 795		} else {
 796			Presences presences = contact.getPresences();
 797			if (presences.size() == 0) {
 798				if (!contact.getOption(Contact.Options.TO)
 799						&& !contact.getOption(Contact.Options.ASKING)
 800						&& contact.getAccount().getStatus() == Account.State.ONLINE) {
 801					showAskForPresenceDialog(contact);
 802				} else if (!contact.getOption(Contact.Options.TO)
 803						|| !contact.getOption(Contact.Options.FROM)) {
 804					warnMutalPresenceSubscription(conversation, listener);
 805				} else {
 806					conversation.setNextCounterpart(null);
 807					listener.onPresenceSelected();
 808				}
 809			} else if (presences.size() == 1) {
 810				String presence = presences.asStringArray()[0];
 811				try {
 812					conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence));
 813				} catch (InvalidJidException e) {
 814					conversation.setNextCounterpart(null);
 815				}
 816				listener.onPresenceSelected();
 817			} else {
 818				final StringBuilder presence = new StringBuilder();
 819				AlertDialog.Builder builder = new AlertDialog.Builder(this);
 820				builder.setTitle(getString(R.string.choose_presence));
 821				final String[] presencesArray = presences.asStringArray();
 822				int preselectedPresence = 0;
 823				for (int i = 0; i < presencesArray.length; ++i) {
 824					if (presencesArray[i].equals(contact.lastseen.presence)) {
 825						preselectedPresence = i;
 826						break;
 827					}
 828				}
 829				presence.append(presencesArray[preselectedPresence]);
 830				builder.setSingleChoiceItems(presencesArray,
 831						preselectedPresence,
 832						new DialogInterface.OnClickListener() {
 833
 834							@Override
 835							public void onClick(DialogInterface dialog,
 836									int which) {
 837								presence.delete(0, presence.length());
 838								presence.append(presencesArray[which]);
 839							}
 840						});
 841				builder.setNegativeButton(R.string.cancel, null);
 842				builder.setPositiveButton(R.string.ok, new OnClickListener() {
 843
 844					@Override
 845					public void onClick(DialogInterface dialog, int which) {
 846						try {
 847							conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString()));
 848						} catch (InvalidJidException e) {
 849							conversation.setNextCounterpart(null);
 850						}
 851						listener.onPresenceSelected();
 852					}
 853				});
 854				builder.create().show();
 855			}
 856		}
 857	}
 858
 859	protected void onActivityResult(int requestCode, int resultCode,
 860			final Intent data) {
 861		super.onActivityResult(requestCode, resultCode, data);
 862		if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
 863			mPendingConferenceInvite = ConferenceInvite.parse(data);
 864			if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
 865				mPendingConferenceInvite.execute(this);
 866				mPendingConferenceInvite = null;
 867			}
 868		}
 869	}
 870
 871	private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
 872		@Override
 873		public void success(final Conversation conversation) {
 874			switchToConversation(conversation);
 875			runOnUiThread(new Runnable() {
 876				@Override
 877				public void run() {
 878					Toast.makeText(XmppActivity.this,R.string.conference_created,Toast.LENGTH_LONG).show();
 879				}
 880			});
 881		}
 882
 883		@Override
 884		public void error(final int errorCode, Conversation object) {
 885			runOnUiThread(new Runnable() {
 886				@Override
 887				public void run() {
 888					Toast.makeText(XmppActivity.this,errorCode,Toast.LENGTH_LONG).show();
 889				}
 890			});
 891		}
 892
 893		@Override
 894		public void userInputRequried(PendingIntent pi, Conversation object) {
 895
 896		}
 897	};
 898
 899	public int getTertiaryTextColor() {
 900		return this.mTertiaryTextColor;
 901	}
 902
 903	public int getSecondaryTextColor() {
 904		return this.mSecondaryTextColor;
 905	}
 906
 907	public int getPrimaryTextColor() {
 908		return this.mPrimaryTextColor;
 909	}
 910
 911	public int getWarningTextColor() {
 912		return this.mColorRed;
 913	}
 914
 915	public int getOnlineColor() {
 916		return this.mColorGreen;
 917	}
 918
 919	public int getPrimaryBackgroundColor() {
 920		return this.mPrimaryBackgroundColor;
 921	}
 922
 923	public int getSecondaryBackgroundColor() {
 924		return this.mSecondaryBackgroundColor;
 925	}
 926
 927	public int getPixel(int dp) {
 928		DisplayMetrics metrics = getResources().getDisplayMetrics();
 929		return ((int) (dp * metrics.density));
 930	}
 931
 932	public boolean copyTextToClipboard(String text, int labelResId) {
 933		ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
 934		String label = getResources().getString(labelResId);
 935		if (mClipBoardManager != null) {
 936			ClipData mClipData = ClipData.newPlainText(label, text);
 937			mClipBoardManager.setPrimaryClip(mClipData);
 938			return true;
 939		}
 940		return false;
 941	}
 942
 943	protected void registerNdefPushMessageCallback() {
 944		NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
 945		if (nfcAdapter != null && nfcAdapter.isEnabled()) {
 946			nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
 947				@Override
 948				public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
 949					return new NdefMessage(new NdefRecord[]{
 950						NdefRecord.createUri(getShareableUri()),
 951							NdefRecord.createApplicationRecord("eu.siacs.conversations")
 952					});
 953				}
 954			}, this);
 955		}
 956	}
 957
 958	protected void unregisterNdefPushMessageCallback() {
 959		NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
 960		if (nfcAdapter != null && nfcAdapter.isEnabled()) {
 961			nfcAdapter.setNdefPushMessageCallback(null,this);
 962		}
 963	}
 964
 965	protected String getShareableUri() {
 966		return null;
 967	}
 968
 969	@Override
 970	public void onResume() {
 971		super.onResume();
 972		if (this.getShareableUri()!=null) {
 973			this.registerNdefPushMessageCallback();
 974		}
 975	}
 976
 977	protected int findTheme() {
 978		if (getPreferences().getBoolean("use_larger_font", false)) {
 979			return R.style.ConversationsTheme_LargerText;
 980		} else {
 981			return R.style.ConversationsTheme;
 982		}
 983	}
 984
 985	@Override
 986	public void onPause() {
 987		super.onPause();
 988		this.unregisterNdefPushMessageCallback();
 989	}
 990
 991	protected void showQrCode() {
 992		String uri = getShareableUri();
 993		if (uri!=null) {
 994			Point size = new Point();
 995			getWindowManager().getDefaultDisplay().getSize(size);
 996			final int width = (size.x < size.y ? size.x : size.y);
 997			Bitmap bitmap = createQrCodeBitmap(uri, width);
 998			ImageView view = new ImageView(this);
 999			view.setImageBitmap(bitmap);
1000			AlertDialog.Builder builder = new AlertDialog.Builder(this);
1001			builder.setView(view);
1002			builder.create().show();
1003		}
1004	}
1005
1006	protected Bitmap createQrCodeBitmap(String input, int size) {
1007		Log.d(Config.LOGTAG,"qr code requested size: "+size);
1008		try {
1009			final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
1010			final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
1011			hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
1012			final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
1013			final int width = result.getWidth();
1014			final int height = result.getHeight();
1015			final int[] pixels = new int[width * height];
1016			for (int y = 0; y < height; y++) {
1017				final int offset = y * width;
1018				for (int x = 0; x < width; x++) {
1019					pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
1020				}
1021			}
1022			final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1023			Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
1024			bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
1025			return bitmap;
1026		} catch (final WriterException e) {
1027			return null;
1028		}
1029	}
1030
1031	public static class ConferenceInvite {
1032		private String uuid;
1033		private List<Jid> jids = new ArrayList<>();
1034
1035		public static ConferenceInvite parse(Intent data) {
1036			ConferenceInvite invite = new ConferenceInvite();
1037			invite.uuid = data.getStringExtra("conversation");
1038			if (invite.uuid == null) {
1039				return null;
1040			}
1041			try {
1042				if (data.getBooleanExtra("multiple", false)) {
1043					String[] toAdd = data.getStringArrayExtra("contacts");
1044					for (String item : toAdd) {
1045						invite.jids.add(Jid.fromString(item));
1046					}
1047				} else {
1048					invite.jids.add(Jid.fromString(data.getStringExtra("contact")));
1049				}
1050			} catch (final InvalidJidException ignored) {
1051				return null;
1052			}
1053			return invite;
1054		}
1055
1056		public void execute(XmppActivity activity) {
1057			XmppConnectionService service = activity.xmppConnectionService;
1058			Conversation conversation = service.findConversationByUuid(this.uuid);
1059			if (conversation == null) {
1060				return;
1061			}
1062			if (conversation.getMode() == Conversation.MODE_MULTI) {
1063				for (Jid jid : jids) {
1064					service.invite(conversation, jid);
1065				}
1066			} else {
1067				jids.add(conversation.getJid().toBareJid());
1068				service.createAdhocConference(conversation.getAccount(), jids, activity.adhocCallback);
1069			}
1070		}
1071	}
1072
1073	public AvatarService avatarService() {
1074		return xmppConnectionService.getAvatarService();
1075	}
1076
1077	class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
1078		private final WeakReference<ImageView> imageViewReference;
1079		private Message message = null;
1080
1081		public BitmapWorkerTask(ImageView imageView) {
1082			imageViewReference = new WeakReference<>(imageView);
1083		}
1084
1085		@Override
1086		protected Bitmap doInBackground(Message... params) {
1087			message = params[0];
1088			try {
1089				return xmppConnectionService.getFileBackend().getThumbnail(
1090						message, (int) (metrics.density * 288), false);
1091			} catch (FileNotFoundException e) {
1092				return null;
1093			}
1094		}
1095
1096		@Override
1097		protected void onPostExecute(Bitmap bitmap) {
1098			if (bitmap != null) {
1099				final ImageView imageView = imageViewReference.get();
1100				if (imageView != null) {
1101					imageView.setImageBitmap(bitmap);
1102					imageView.setBackgroundColor(0x00000000);
1103				}
1104			}
1105		}
1106	}
1107
1108	public void loadBitmap(Message message, ImageView imageView) {
1109		Bitmap bm;
1110		try {
1111			bm = xmppConnectionService.getFileBackend().getThumbnail(message,
1112					(int) (metrics.density * 288), true);
1113		} catch (FileNotFoundException e) {
1114			bm = null;
1115		}
1116		if (bm != null) {
1117			imageView.setImageBitmap(bm);
1118			imageView.setBackgroundColor(0x00000000);
1119		} else {
1120			if (cancelPotentialWork(message, imageView)) {
1121				imageView.setBackgroundColor(0xff333333);
1122				final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1123				final AsyncDrawable asyncDrawable = new AsyncDrawable(
1124						getResources(), null, task);
1125				imageView.setImageDrawable(asyncDrawable);
1126				try {
1127					task.execute(message);
1128				} catch (final RejectedExecutionException ignored) {
1129				}
1130			}
1131		}
1132	}
1133
1134	public static boolean cancelPotentialWork(Message message,
1135			ImageView imageView) {
1136		final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
1137
1138		if (bitmapWorkerTask != null) {
1139			final Message oldMessage = bitmapWorkerTask.message;
1140			if (oldMessage == null || message != oldMessage) {
1141				bitmapWorkerTask.cancel(true);
1142			} else {
1143				return false;
1144			}
1145		}
1146		return true;
1147	}
1148
1149	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
1150		if (imageView != null) {
1151			final Drawable drawable = imageView.getDrawable();
1152			if (drawable instanceof AsyncDrawable) {
1153				final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
1154				return asyncDrawable.getBitmapWorkerTask();
1155			}
1156		}
1157		return null;
1158	}
1159
1160	static class AsyncDrawable extends BitmapDrawable {
1161		private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1162
1163		public AsyncDrawable(Resources res, Bitmap bitmap,
1164				BitmapWorkerTask bitmapWorkerTask) {
1165			super(res, bitmap);
1166			bitmapWorkerTaskReference = new WeakReference<>(
1167					bitmapWorkerTask);
1168		}
1169
1170		public BitmapWorkerTask getBitmapWorkerTask() {
1171			return bitmapWorkerTaskReference.get();
1172		}
1173	}
1174}