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