XmppActivity.java

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