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