XmppActivity.java

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