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