XmppActivity.java

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