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