XmppActivity.java

   1package eu.siacs.conversations.ui;
   2
   3import android.Manifest;
   4import android.annotation.SuppressLint;
   5import android.annotation.TargetApi;
   6import android.support.v7.app.ActionBar;
   7import android.support.v7.app.AlertDialog;
   8import android.support.v7.app.AlertDialog.Builder;
   9import android.app.PendingIntent;
  10import android.content.ActivityNotFoundException;
  11import android.content.ClipData;
  12import android.content.ClipboardManager;
  13import android.content.ComponentName;
  14import android.content.Context;
  15import android.content.DialogInterface;
  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.content.res.TypedArray;
  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.ConnectivityManager;
  30import android.net.Uri;
  31import android.os.AsyncTask;
  32import android.os.Build;
  33import android.os.Bundle;
  34import android.os.Handler;
  35import android.os.IBinder;
  36import android.os.PowerManager;
  37import android.os.SystemClock;
  38import android.preference.PreferenceManager;
  39import android.support.v4.content.ContextCompat;
  40import android.support.v7.app.AppCompatActivity;
  41import android.support.v7.app.AppCompatDelegate;
  42import android.text.InputType;
  43import android.util.DisplayMetrics;
  44import android.util.Log;
  45import android.view.Menu;
  46import android.view.MenuItem;
  47import android.view.View;
  48import android.view.inputmethod.InputMethodManager;
  49import android.widget.EditText;
  50import android.widget.ImageView;
  51import android.widget.Toast;
  52
  53import java.io.FileNotFoundException;
  54import java.lang.ref.WeakReference;
  55import java.util.ArrayList;
  56import java.util.List;
  57import java.util.concurrent.RejectedExecutionException;
  58
  59import eu.siacs.conversations.Config;
  60import eu.siacs.conversations.R;
  61import eu.siacs.conversations.crypto.PgpEngine;
  62import eu.siacs.conversations.entities.Account;
  63import eu.siacs.conversations.entities.Contact;
  64import eu.siacs.conversations.entities.Conversation;
  65import eu.siacs.conversations.entities.Message;
  66import eu.siacs.conversations.entities.Presences;
  67import eu.siacs.conversations.services.AvatarService;
  68import eu.siacs.conversations.services.BarcodeProvider;
  69import eu.siacs.conversations.services.XmppConnectionService;
  70import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
  71import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
  72import eu.siacs.conversations.ui.util.PresenceSelector;
  73import eu.siacs.conversations.utils.ExceptionHelper;
  74import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
  75import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
  76import rocks.xmpp.addr.Jid;
  77
  78public abstract class XmppActivity extends AppCompatActivity {
  79
  80	public static final String EXTRA_ACCOUNT = "account";
  81	protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
  82	protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
  83	protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103;
  84	protected static final int REQUEST_BATTERY_OP = 0x49ff;
  85	public XmppConnectionService xmppConnectionService;
  86	public boolean xmppConnectionServiceBound = false;
  87	protected boolean registeredListeners = false;
  88
  89	protected int mColorRed;
  90	protected int mColorOrange;
  91	protected int mColorGreen;
  92
  93	protected static final String FRAGMENT_TAG_DIALOG = "dialog";
  94
  95	private boolean isCameraFeatureAvailable = false;
  96
  97	protected boolean mUseSubject = true;
  98	protected int mTheme;
  99	protected boolean mUsingEnterKey = false;
 100	protected Toast mToast;
 101	public Runnable onOpenPGPKeyPublished = () -> Toast.makeText(XmppActivity.this, R.string.openpgp_has_been_published, Toast.LENGTH_SHORT).show();
 102	protected ConferenceInvite mPendingConferenceInvite = null;
 103	protected ServiceConnection mConnection = new ServiceConnection() {
 104
 105		@Override
 106		public void onServiceConnected(ComponentName className, IBinder service) {
 107			XmppConnectionBinder binder = (XmppConnectionBinder) service;
 108			xmppConnectionService = binder.getService();
 109			xmppConnectionServiceBound = true;
 110			if (!registeredListeners && shouldRegisterListeners()) {
 111				registerListeners();
 112				registeredListeners = true;
 113			}
 114			onBackendConnected();
 115		}
 116
 117		@Override
 118		public void onServiceDisconnected(ComponentName arg0) {
 119			xmppConnectionServiceBound = false;
 120		}
 121	};
 122	private DisplayMetrics metrics;
 123	private long mLastUiRefresh = 0;
 124	private Handler mRefreshUiHandler = new Handler();
 125	private Runnable mRefreshUiRunnable = () -> {
 126		mLastUiRefresh = SystemClock.elapsedRealtime();
 127		refreshUiReal();
 128	};
 129	private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
 130		@Override
 131		public void success(final Conversation conversation) {
 132			runOnUiThread(() -> {
 133				switchToConversation(conversation);
 134				hideToast();
 135			});
 136		}
 137
 138		@Override
 139		public void error(final int errorCode, Conversation object) {
 140			runOnUiThread(() -> replaceToast(getString(errorCode)));
 141		}
 142
 143		@Override
 144		public void userInputRequried(PendingIntent pi, Conversation object) {
 145
 146		}
 147	};
 148	public boolean mSkipBackgroundBinding = false;
 149
 150	public static boolean cancelPotentialWork(Message message, ImageView imageView) {
 151		final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
 152
 153		if (bitmapWorkerTask != null) {
 154			final Message oldMessage = bitmapWorkerTask.message;
 155			if (oldMessage == null || message != oldMessage) {
 156				bitmapWorkerTask.cancel(true);
 157			} else {
 158				return false;
 159			}
 160		}
 161		return true;
 162	}
 163
 164	private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
 165		if (imageView != null) {
 166			final Drawable drawable = imageView.getDrawable();
 167			if (drawable instanceof AsyncDrawable) {
 168				final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
 169				return asyncDrawable.getBitmapWorkerTask();
 170			}
 171		}
 172		return null;
 173	}
 174
 175	protected void hideToast() {
 176		if (mToast != null) {
 177			mToast.cancel();
 178		}
 179	}
 180
 181	protected void replaceToast(String msg) {
 182		replaceToast(msg, true);
 183	}
 184
 185	protected void replaceToast(String msg, boolean showlong) {
 186		hideToast();
 187		mToast = Toast.makeText(this, msg, showlong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT);
 188		mToast.show();
 189	}
 190
 191	protected final void refreshUi() {
 192		final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh;
 193		if (diff > Config.REFRESH_UI_INTERVAL) {
 194			mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
 195			runOnUiThread(mRefreshUiRunnable);
 196		} else {
 197			final long next = Config.REFRESH_UI_INTERVAL - diff;
 198			mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
 199			mRefreshUiHandler.postDelayed(mRefreshUiRunnable, next);
 200		}
 201	}
 202
 203	abstract protected void refreshUiReal();
 204
 205	@Override
 206	protected void onStart() {
 207		super.onStart();
 208		if (!xmppConnectionServiceBound) {
 209			if (this.mSkipBackgroundBinding) {
 210				Log.d(Config.LOGTAG,"skipping background binding");
 211			} else {
 212				connectToBackend();
 213			}
 214		} else {
 215			if (!registeredListeners) {
 216				this.registerListeners();
 217				this.registeredListeners = true;
 218			}
 219			this.onBackendConnected();
 220		}
 221	}
 222
 223	@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
 224	protected boolean shouldRegisterListeners() {
 225		if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
 226			return !isDestroyed() && !isFinishing();
 227		} else {
 228			return !isFinishing();
 229		}
 230	}
 231
 232	public void connectToBackend() {
 233		Intent intent = new Intent(this, XmppConnectionService.class);
 234		intent.setAction("ui");
 235		startService(intent);
 236		bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
 237	}
 238
 239	@Override
 240	protected void onStop() {
 241		super.onStop();
 242		if (xmppConnectionServiceBound) {
 243			if (registeredListeners) {
 244				this.unregisterListeners();
 245				this.registeredListeners = false;
 246			}
 247			unbindService(mConnection);
 248			xmppConnectionServiceBound = false;
 249		}
 250	}
 251
 252	protected void hideKeyboard() {
 253		final InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
 254		View focus = getCurrentFocus();
 255		if (focus != null && inputManager != null) {
 256			inputManager.hideSoftInputFromWindow(focus.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
 257		}
 258	}
 259
 260	public boolean hasPgp() {
 261		return xmppConnectionService.getPgpEngine() != null;
 262	}
 263
 264	public void showInstallPgpDialog() {
 265		Builder builder = new AlertDialog.Builder(this);
 266		builder.setTitle(getString(R.string.openkeychain_required));
 267		builder.setIconAttribute(android.R.attr.alertDialogIcon);
 268		builder.setMessage(getText(R.string.openkeychain_required_long));
 269		builder.setNegativeButton(getString(R.string.cancel), null);
 270		builder.setNeutralButton(getString(R.string.restart),
 271				(dialog, which) -> {
 272					if (xmppConnectionServiceBound) {
 273						unbindService(mConnection);
 274						xmppConnectionServiceBound = false;
 275					}
 276					stopService(new Intent(XmppActivity.this,
 277							XmppConnectionService.class));
 278					finish();
 279				});
 280		builder.setPositiveButton(getString(R.string.install),
 281				(dialog, which) -> {
 282					Uri uri = Uri
 283							.parse("market://details?id=org.sufficientlysecure.keychain");
 284					Intent marketIntent = new Intent(Intent.ACTION_VIEW,
 285							uri);
 286					PackageManager manager = getApplicationContext()
 287							.getPackageManager();
 288					List<ResolveInfo> infos = manager
 289							.queryIntentActivities(marketIntent, 0);
 290					if (infos.size() > 0) {
 291						startActivity(marketIntent);
 292					} else {
 293						uri = Uri.parse("http://www.openkeychain.org/");
 294						Intent browserIntent = new Intent(
 295								Intent.ACTION_VIEW, uri);
 296						startActivity(browserIntent);
 297					}
 298					finish();
 299				});
 300		builder.create().show();
 301	}
 302
 303	abstract void onBackendConnected();
 304
 305	protected void registerListeners() {
 306		if (this instanceof XmppConnectionService.OnConversationUpdate) {
 307			this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this);
 308		}
 309		if (this instanceof XmppConnectionService.OnAccountUpdate) {
 310			this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
 311		}
 312		if (this instanceof XmppConnectionService.OnCaptchaRequested) {
 313			this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
 314		}
 315		if (this instanceof XmppConnectionService.OnRosterUpdate) {
 316			this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
 317		}
 318		if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
 319			this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this);
 320		}
 321		if (this instanceof OnUpdateBlocklist) {
 322			this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this);
 323		}
 324		if (this instanceof XmppConnectionService.OnShowErrorToast) {
 325			this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
 326		}
 327		if (this instanceof OnKeyStatusUpdated) {
 328			this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this);
 329		}
 330	}
 331
 332	protected void unregisterListeners() {
 333		if (this instanceof XmppConnectionService.OnConversationUpdate) {
 334			this.xmppConnectionService.removeOnConversationListChangedListener();
 335		}
 336		if (this instanceof XmppConnectionService.OnAccountUpdate) {
 337			this.xmppConnectionService.removeOnAccountListChangedListener();
 338		}
 339		if (this instanceof XmppConnectionService.OnCaptchaRequested) {
 340			this.xmppConnectionService.removeOnCaptchaRequestedListener();
 341		}
 342		if (this instanceof XmppConnectionService.OnRosterUpdate) {
 343			this.xmppConnectionService.removeOnRosterUpdateListener();
 344		}
 345		if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
 346			this.xmppConnectionService.removeOnMucRosterUpdateListener();
 347		}
 348		if (this instanceof OnUpdateBlocklist) {
 349			this.xmppConnectionService.removeOnUpdateBlocklistListener();
 350		}
 351		if (this instanceof XmppConnectionService.OnShowErrorToast) {
 352			this.xmppConnectionService.removeOnShowErrorToastListener();
 353		}
 354		if (this instanceof OnKeyStatusUpdated) {
 355			this.xmppConnectionService.removeOnNewKeysAvailableListener();
 356		}
 357	}
 358
 359	@Override
 360	public boolean onOptionsItemSelected(final MenuItem item) {
 361		switch (item.getItemId()) {
 362			case R.id.action_settings:
 363				startActivity(new Intent(this, SettingsActivity.class));
 364				break;
 365			case R.id.action_accounts:
 366				startActivity(new Intent(this, ManageAccountActivity.class));
 367				break;
 368			case android.R.id.home:
 369				finish();
 370				break;
 371			case R.id.action_show_qr_code:
 372				showQrCode();
 373				break;
 374		}
 375		return super.onOptionsItemSelected(item);
 376	}
 377
 378	public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
 379		final Contact contact = conversation.getContact();
 380		if (!contact.showInRoster()) {
 381			showAddToRosterDialog(conversation.getContact());
 382		} else {
 383			final Presences presences = contact.getPresences();
 384			if (presences.size() == 0) {
 385				if (!contact.getOption(Contact.Options.TO)
 386						&& !contact.getOption(Contact.Options.ASKING)
 387						&& contact.getAccount().getStatus() == Account.State.ONLINE) {
 388					showAskForPresenceDialog(contact);
 389				} else if (!contact.getOption(Contact.Options.TO)
 390						|| !contact.getOption(Contact.Options.FROM)) {
 391					PresenceSelector.warnMutualPresenceSubscription(this, conversation, listener);
 392				} else {
 393					conversation.setNextCounterpart(null);
 394					listener.onPresenceSelected();
 395				}
 396			} else if (presences.size() == 1) {
 397				String presence = presences.toResourceArray()[0];
 398				try {
 399					conversation.setNextCounterpart(Jid.of(contact.getJid().getLocal(), contact.getJid().getDomain(), presence));
 400				} catch (IllegalArgumentException e) {
 401					conversation.setNextCounterpart(null);
 402				}
 403				listener.onPresenceSelected();
 404			} else {
 405				PresenceSelector.showPresenceSelectionDialog(this, conversation, listener);
 406			}
 407		}
 408	}
 409
 410	@Override
 411	protected void onCreate(Bundle savedInstanceState) {
 412		super.onCreate(savedInstanceState);
 413		metrics = getResources().getDisplayMetrics();
 414		ExceptionHelper.init(getApplicationContext());
 415		this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
 416
 417		mColorRed = ContextCompat.getColor(this, R.color.red800);
 418		mColorOrange = ContextCompat.getColor(this, R.color.orange500);
 419		mColorGreen = ContextCompat.getColor(this, R.color.green500);
 420
 421		this.mTheme = findTheme();
 422		setTheme(this.mTheme);
 423
 424		this.mUsingEnterKey = usingEnterKey();
 425		mUseSubject = getPreferences().getBoolean("use_subject", getResources().getBoolean(R.bool.use_subject));
 426	}
 427
 428	protected boolean isCameraFeatureAvailable() {
 429		return this.isCameraFeatureAvailable;
 430	}
 431
 432	public boolean isDarkTheme() {
 433		return this.mTheme == R.style.ConversationsTheme_Dark;
 434	}
 435
 436	public int getThemeResource(int r_attr_name, int r_drawable_def) {
 437		int[] attrs = {r_attr_name};
 438		TypedArray ta = this.getTheme().obtainStyledAttributes(attrs);
 439
 440		int res = ta.getResourceId(0, r_drawable_def);
 441		ta.recycle();
 442
 443		return res;
 444	}
 445
 446	protected boolean isOptimizingBattery() {
 447		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 448			final PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
 449			return pm != null
 450					&& !pm.isIgnoringBatteryOptimizations(getPackageName());
 451		} else {
 452			return false;
 453		}
 454	}
 455
 456	protected boolean isAffectedByDataSaver() {
 457		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 458			final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
 459			return cm != null
 460					&& cm.isActiveNetworkMetered()
 461					&& cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
 462		} else {
 463			return false;
 464		}
 465	}
 466
 467	protected boolean usingEnterKey() {
 468		return getPreferences().getBoolean("display_enter_key", getResources().getBoolean(R.bool.display_enter_key));
 469	}
 470
 471	protected SharedPreferences getPreferences() {
 472		return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
 473	}
 474
 475	public boolean useSubjectToIdentifyConference() {
 476		return mUseSubject;
 477	}
 478
 479	public void switchToConversation(Conversation conversation) {
 480		switchToConversation(conversation, null, false);
 481	}
 482
 483	public void switchToConversation(Conversation conversation, String text,
 484	                                 boolean newTask) {
 485		switchToConversation(conversation, text, null, false, newTask);
 486	}
 487
 488	public void highlightInMuc(Conversation conversation, String nick) {
 489		switchToConversation(conversation, null, nick, false, false);
 490	}
 491
 492	public void privateMsgInMuc(Conversation conversation, String nick) {
 493		switchToConversation(conversation, null, nick, true, false);
 494	}
 495
 496	private void switchToConversation(Conversation conversation, String text, String nick, boolean pm, boolean newTask) {
 497		Intent intent = new Intent(this, ConversationsActivity.class);
 498		intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
 499		intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
 500		if (text != null) {
 501			intent.putExtra(ConversationsActivity.EXTRA_TEXT, text);
 502		}
 503		if (nick != null) {
 504			intent.putExtra(ConversationsActivity.EXTRA_NICK, nick);
 505			intent.putExtra(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, pm);
 506		}
 507		if (newTask) {
 508			intent.setFlags(intent.getFlags()
 509					| Intent.FLAG_ACTIVITY_NEW_TASK
 510					| Intent.FLAG_ACTIVITY_SINGLE_TOP);
 511		} else {
 512			intent.setFlags(intent.getFlags()
 513					| Intent.FLAG_ACTIVITY_CLEAR_TOP);
 514		}
 515		startActivity(intent);
 516		finish();
 517	}
 518
 519	public void switchToContactDetails(Contact contact) {
 520		switchToContactDetails(contact, null);
 521	}
 522
 523	public void switchToContactDetails(Contact contact, String messageFingerprint) {
 524		Intent intent = new Intent(this, ContactDetailsActivity.class);
 525		intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
 526		intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().asBareJid().toString());
 527		intent.putExtra("contact", contact.getJid().toString());
 528		intent.putExtra("fingerprint", messageFingerprint);
 529		startActivity(intent);
 530	}
 531
 532	public void switchToAccount(Account account) {
 533		switchToAccount(account, false);
 534	}
 535
 536	public void switchToAccount(Account account, boolean init) {
 537		Intent intent = new Intent(this, EditAccountActivity.class);
 538		intent.putExtra("jid", account.getJid().asBareJid().toString());
 539		intent.putExtra("init", init);
 540		if (init) {
 541			intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
 542		}
 543		startActivity(intent);
 544		if (init) {
 545			overridePendingTransition(0, 0);
 546		}
 547	}
 548
 549	protected void delegateUriPermissionsToService(Uri uri) {
 550		Intent intent = new Intent(this,XmppConnectionService.class);
 551		intent.setAction(Intent.ACTION_SEND);
 552		intent.setData(uri);
 553		intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
 554		startService(intent);
 555	}
 556
 557	protected void inviteToConversation(Conversation conversation) {
 558		startActivityForResult(ChooseContactActivity.create(this,conversation), REQUEST_INVITE_TO_CONVERSATION);
 559	}
 560
 561	protected void announcePgp(final Account account, final Conversation conversation, Intent intent, final Runnable onSuccess) {
 562		if (account.getPgpId() == 0) {
 563			choosePgpSignId(account);
 564		} else {
 565			String status = null;
 566			if (manuallyChangePresence()) {
 567				status = account.getPresenceStatusMessage();
 568			}
 569			if (status == null) {
 570				status = "";
 571			}
 572			xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback<String>() {
 573
 574				@Override
 575				public void userInputRequried(PendingIntent pi, String signature) {
 576					try {
 577						startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
 578					} catch (final SendIntentException ignored) {
 579					}
 580				}
 581
 582				@Override
 583				public void success(String signature) {
 584					account.setPgpSignature(signature);
 585					xmppConnectionService.databaseBackend.updateAccount(account);
 586					xmppConnectionService.sendPresence(account);
 587					if (conversation != null) {
 588						conversation.setNextEncryption(Message.ENCRYPTION_PGP);
 589						xmppConnectionService.updateConversation(conversation);
 590						refreshUi();
 591					}
 592					if (onSuccess != null) {
 593						runOnUiThread(onSuccess);
 594					}
 595				}
 596
 597				@Override
 598				public void error(int error, String signature) {
 599					if (error == 0) {
 600						account.setPgpSignId(0);
 601						account.unsetPgpSignature();
 602						xmppConnectionService.databaseBackend.updateAccount(account);
 603						choosePgpSignId(account);
 604					} else {
 605						displayErrorDialog(error);
 606					}
 607				}
 608			});
 609		}
 610	}
 611
 612	public static void configureActionBar(ActionBar actionBar) {
 613		if (actionBar != null) {
 614			actionBar.setHomeButtonEnabled(true);
 615			actionBar.setDisplayHomeAsUpEnabled(true);
 616		}
 617	}
 618
 619	protected boolean noAccountUsesPgp() {
 620		if (!hasPgp()) {
 621			return true;
 622		}
 623		for (Account account : xmppConnectionService.getAccounts()) {
 624			if (account.getPgpId() != 0) {
 625				return false;
 626			}
 627		}
 628		return true;
 629	}
 630
 631	@SuppressWarnings("deprecation")
 632	@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
 633	protected void setListItemBackgroundOnView(View view) {
 634		int sdk = android.os.Build.VERSION.SDK_INT;
 635		if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) {
 636			view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground));
 637		} else {
 638			view.setBackground(getResources().getDrawable(R.drawable.greybackground));
 639		}
 640	}
 641
 642	protected void choosePgpSignId(Account account) {
 643		xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<Account>() {
 644			@Override
 645			public void success(Account account1) {
 646			}
 647
 648			@Override
 649			public void error(int errorCode, Account object) {
 650
 651			}
 652
 653			@Override
 654			public void userInputRequried(PendingIntent pi, Account object) {
 655				try {
 656					startIntentSenderForResult(pi.getIntentSender(),
 657							REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0);
 658				} catch (final SendIntentException ignored) {
 659				}
 660			}
 661		});
 662	}
 663
 664	protected void displayErrorDialog(final int errorCode) {
 665		runOnUiThread(() -> {
 666			Builder builder = new Builder(XmppActivity.this);
 667			builder.setIconAttribute(android.R.attr.alertDialogIcon);
 668			builder.setTitle(getString(R.string.error));
 669			builder.setMessage(errorCode);
 670			builder.setNeutralButton(R.string.accept, null);
 671			builder.create().show();
 672		});
 673
 674	}
 675
 676	protected void showAddToRosterDialog(final Contact contact) {
 677		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 678		builder.setTitle(contact.getJid().toString());
 679		builder.setMessage(getString(R.string.not_in_roster));
 680		builder.setNegativeButton(getString(R.string.cancel), null);
 681		builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> xmppConnectionService.createContact(contact,true));
 682		builder.create().show();
 683	}
 684
 685	private void showAskForPresenceDialog(final Contact contact) {
 686		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 687		builder.setTitle(contact.getJid().toString());
 688		builder.setMessage(R.string.request_presence_updates);
 689		builder.setNegativeButton(R.string.cancel, null);
 690		builder.setPositiveButton(R.string.request_now,
 691				(dialog, which) -> {
 692					if (xmppConnectionServiceBound) {
 693						xmppConnectionService.sendPresencePacket(contact
 694								.getAccount(), xmppConnectionService
 695								.getPresenceGenerator()
 696								.requestPresenceUpdatesFrom(contact));
 697					}
 698				});
 699		builder.create().show();
 700	}
 701
 702	protected void quickEdit(String previousValue, int hint, OnValueEdited callback) {
 703		quickEdit(previousValue, callback, hint, false);
 704	}
 705
 706	protected void quickPasswordEdit(String previousValue, OnValueEdited callback) {
 707		quickEdit(previousValue, callback, R.string.password, true);
 708	}
 709
 710	@SuppressLint("InflateParams")
 711	private void quickEdit(final String previousValue,
 712	                       final OnValueEdited callback,
 713	                       final int hint,
 714	                       boolean password) {
 715		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 716		View view = getLayoutInflater().inflate(R.layout.quickedit, null);
 717		final EditText editor = view.findViewById(R.id.editor);
 718		if (password) {
 719			editor.setInputType(InputType.TYPE_CLASS_TEXT
 720					| InputType.TYPE_TEXT_VARIATION_PASSWORD);
 721		}
 722		builder.setPositiveButton(R.string.accept, null);
 723		if (hint != 0) {
 724			editor.setHint(hint);
 725		}
 726		editor.requestFocus();
 727		editor.setText("");
 728		if (previousValue != null) {
 729			editor.getText().append(previousValue);
 730		}
 731		builder.setView(view);
 732		builder.setNegativeButton(R.string.cancel, null);
 733		final AlertDialog dialog = builder.create();
 734		dialog.show();
 735		View.OnClickListener clickListener = v -> {
 736			String value = editor.getText().toString();
 737			if (!value.equals(previousValue) && value.trim().length() > 0) {
 738				String error = callback.onValueEdited(value);
 739				if (error != null) {
 740					editor.setError(error);
 741					return;
 742				}
 743			}
 744			dialog.dismiss();
 745		};
 746		dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
 747	}
 748
 749	protected boolean hasStoragePermission(int requestCode) {
 750		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 751			if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
 752				requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
 753				return false;
 754			} else {
 755				return true;
 756			}
 757		} else {
 758			return true;
 759		}
 760	}
 761
 762	protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
 763		super.onActivityResult(requestCode, resultCode, data);
 764		if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
 765			mPendingConferenceInvite = ConferenceInvite.parse(data);
 766			if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
 767				if (mPendingConferenceInvite.execute(this)) {
 768					mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
 769					mToast.show();
 770				}
 771				mPendingConferenceInvite = null;
 772			}
 773		}
 774	}
 775
 776	public int getWarningTextColor() {
 777		return this.mColorRed;
 778	}
 779
 780	public int getOnlineColor() {
 781		return this.mColorGreen;
 782	}
 783
 784	public int getPixel(int dp) {
 785		DisplayMetrics metrics = getResources().getDisplayMetrics();
 786		return ((int) (dp * metrics.density));
 787	}
 788
 789	public boolean copyTextToClipboard(String text, int labelResId) {
 790		ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
 791		String label = getResources().getString(labelResId);
 792		if (mClipBoardManager != null) {
 793			ClipData mClipData = ClipData.newPlainText(label, text);
 794			mClipBoardManager.setPrimaryClip(mClipData);
 795			return true;
 796		}
 797		return false;
 798	}
 799
 800	protected boolean neverCompressPictures() {
 801		return getPreferences().getString("picture_compression", getResources().getString(R.string.picture_compression)).equals("never");
 802	}
 803
 804	protected boolean manuallyChangePresence() {
 805		return getPreferences().getBoolean(SettingsActivity.MANUALLY_CHANGE_PRESENCE, getResources().getBoolean(R.bool.manually_change_presence));
 806	}
 807
 808	protected String getShareableUri() {
 809		return getShareableUri(false);
 810	}
 811
 812	protected String getShareableUri(boolean http) {
 813		return null;
 814	}
 815
 816	protected void shareLink(boolean http) {
 817		String uri = getShareableUri(http);
 818		if (uri == null || uri.isEmpty()) {
 819			return;
 820		}
 821		Intent intent = new Intent(Intent.ACTION_SEND);
 822		intent.setType("text/plain");
 823		intent.putExtra(Intent.EXTRA_TEXT, getShareableUri(http));
 824		try {
 825			startActivity(Intent.createChooser(intent, getText(R.string.share_uri_with)));
 826		} catch (ActivityNotFoundException e) {
 827			Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
 828		}
 829	}
 830
 831	protected void launchOpenKeyChain(long keyId) {
 832		PgpEngine pgp = XmppActivity.this.xmppConnectionService.getPgpEngine();
 833		try {
 834			startIntentSenderForResult(
 835					pgp.getIntentForKey(keyId).getIntentSender(), 0, null, 0,
 836					0, 0);
 837		} catch (Throwable e) {
 838			Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show();
 839		}
 840	}
 841
 842	@Override
 843	public void onResume() {
 844		super.onResume();
 845	}
 846
 847	protected int findTheme() {
 848		Boolean dark = getPreferences().getString(SettingsActivity.THEME, getResources().getString(R.string.theme)).equals("dark");
 849
 850		if (dark) {
 851			return R.style.ConversationsTheme_Dark;
 852		} else {
 853			return R.style.ConversationsTheme;
 854		}
 855	}
 856
 857	@Override
 858	public void onPause() {
 859		super.onPause();
 860	}
 861
 862	@Override
 863	public boolean onMenuOpened(int id, Menu menu) {
 864		if(id == AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR && menu != null) {
 865			MenuDoubleTabUtil.recordMenuOpen();
 866		}
 867		return super.onMenuOpened(id, menu);
 868	}
 869
 870	protected void showQrCode() {
 871		showQrCode(getShareableUri());
 872	}
 873
 874	protected void showQrCode(final String uri) {
 875		if (uri == null || uri.isEmpty()) {
 876			return;
 877		}
 878		Point size = new Point();
 879		getWindowManager().getDefaultDisplay().getSize(size);
 880		final int width = (size.x < size.y ? size.x : size.y);
 881		Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width);
 882		ImageView view = new ImageView(this);
 883		view.setBackgroundColor(Color.WHITE);
 884		view.setImageBitmap(bitmap);
 885		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 886		builder.setView(view);
 887		builder.create().show();
 888	}
 889
 890	protected Account extractAccount(Intent intent) {
 891		String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
 892		try {
 893			return jid != null ? xmppConnectionService.findAccountByJid(Jid.of(jid)) : null;
 894		} catch (IllegalArgumentException e) {
 895			return null;
 896		}
 897	}
 898
 899	public AvatarService avatarService() {
 900		return xmppConnectionService.getAvatarService();
 901	}
 902
 903	public void loadBitmap(Message message, ImageView imageView) {
 904		Bitmap bm;
 905		try {
 906			bm = xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288), true);
 907		} catch (FileNotFoundException e) {
 908			bm = null;
 909		}
 910		if (bm != null) {
 911			cancelPotentialWork(message, imageView);
 912			imageView.setImageBitmap(bm);
 913			imageView.setBackgroundColor(0x00000000);
 914		} else {
 915			if (cancelPotentialWork(message, imageView)) {
 916				imageView.setBackgroundColor(0xff333333);
 917				imageView.setImageDrawable(null);
 918				final BitmapWorkerTask task = new BitmapWorkerTask(this, imageView);
 919				final AsyncDrawable asyncDrawable = new AsyncDrawable(
 920						getResources(), null, task);
 921				imageView.setImageDrawable(asyncDrawable);
 922				try {
 923					task.execute(message);
 924				} catch (final RejectedExecutionException ignored) {
 925					ignored.printStackTrace();
 926				}
 927			}
 928		}
 929	}
 930
 931	protected interface OnValueEdited {
 932		String onValueEdited(String value);
 933	}
 934
 935	public static class ConferenceInvite {
 936		private String uuid;
 937		private List<Jid> jids = new ArrayList<>();
 938
 939		public static ConferenceInvite parse(Intent data) {
 940			ConferenceInvite invite = new ConferenceInvite();
 941			invite.uuid = data.getStringExtra("conversation");
 942			if (invite.uuid == null) {
 943				return null;
 944			}
 945			try {
 946				if (data.getBooleanExtra("multiple", false)) {
 947					String[] toAdd = data.getStringArrayExtra("contacts");
 948					for (String item : toAdd) {
 949						invite.jids.add(Jid.of(item));
 950					}
 951				} else {
 952					invite.jids.add(Jid.of(data.getStringExtra("contact")));
 953				}
 954			} catch (final IllegalArgumentException ignored) {
 955				return null;
 956			}
 957			return invite;
 958		}
 959
 960		public boolean execute(XmppActivity activity) {
 961			XmppConnectionService service = activity.xmppConnectionService;
 962			Conversation conversation = service.findConversationByUuid(this.uuid);
 963			if (conversation == null) {
 964				return false;
 965			}
 966			if (conversation.getMode() == Conversation.MODE_MULTI) {
 967				for (Jid jid : jids) {
 968					service.invite(conversation, jid);
 969				}
 970				return false;
 971			} else {
 972				jids.add(conversation.getJid().asBareJid());
 973				return service.createAdhocConference(conversation.getAccount(), null, jids, activity.adhocCallback);
 974			}
 975		}
 976	}
 977
 978	static class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
 979		private final WeakReference<ImageView> imageViewReference;
 980		private final WeakReference<XmppActivity> activity;
 981		private Message message = null;
 982
 983		private BitmapWorkerTask(XmppActivity activity, ImageView imageView) {
 984			this.activity = new WeakReference<>(activity);
 985			this.imageViewReference = new WeakReference<>(imageView);
 986		}
 987
 988		@Override
 989		protected Bitmap doInBackground(Message... params) {
 990			if (isCancelled()) {
 991				return null;
 992			}
 993			message = params[0];
 994			try {
 995				XmppActivity activity = this.activity.get();
 996				if (activity != null && activity.xmppConnectionService != null) {
 997					return activity.xmppConnectionService.getFileBackend().getThumbnail(message, (int) (activity.metrics.density * 288), false);
 998				} else {
 999					return null;
1000				}
1001			} catch (FileNotFoundException e) {
1002				return null;
1003			}
1004		}
1005
1006		@Override
1007		protected void onPostExecute(Bitmap bitmap) {
1008			if (bitmap != null && !isCancelled()) {
1009				final ImageView imageView = imageViewReference.get();
1010				if (imageView != null) {
1011					imageView.setImageBitmap(bitmap);
1012					imageView.setBackgroundColor(0x00000000);
1013				}
1014			}
1015		}
1016	}
1017
1018	private static class AsyncDrawable extends BitmapDrawable {
1019		private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1020
1021		private AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
1022			super(res, bitmap);
1023			bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
1024		}
1025
1026		private BitmapWorkerTask getBitmapWorkerTask() {
1027			return bitmapWorkerTaskReference.get();
1028		}
1029	}
1030}