StartConversationActivity.java

   1package eu.siacs.conversations.ui;
   2
   3import android.Manifest;
   4import android.annotation.SuppressLint;
   5import android.app.Dialog;
   6import android.app.PendingIntent;
   7import android.content.ActivityNotFoundException;
   8import android.content.Context;
   9import android.content.Intent;
  10import android.content.pm.PackageManager;
  11import android.databinding.DataBindingUtil;
  12import android.net.Uri;
  13import android.os.Build;
  14import android.os.Bundle;
  15import android.support.annotation.DrawableRes;
  16import android.support.annotation.NonNull;
  17import android.support.annotation.Nullable;
  18import android.support.v4.app.Fragment;
  19import android.support.v4.app.FragmentManager;
  20import android.support.v4.app.FragmentTransaction;
  21import android.support.v4.app.ListFragment;
  22import android.support.v4.view.PagerAdapter;
  23import android.support.v4.view.ViewPager;
  24import android.support.v7.app.ActionBar;
  25import android.support.v7.app.AlertDialog;
  26import android.support.v7.widget.Toolbar;
  27import android.text.Editable;
  28import android.text.SpannableString;
  29import android.text.Spanned;
  30import android.text.TextWatcher;
  31import android.text.style.TypefaceSpan;
  32import android.util.Log;
  33import android.util.Pair;
  34import android.view.ContextMenu;
  35import android.view.ContextMenu.ContextMenuInfo;
  36import android.view.KeyEvent;
  37import android.view.Menu;
  38import android.view.MenuItem;
  39import android.view.View;
  40import android.view.ViewGroup;
  41import android.view.inputmethod.InputMethodManager;
  42import android.widget.AdapterView;
  43import android.widget.AdapterView.AdapterContextMenuInfo;
  44import android.widget.ArrayAdapter;
  45import android.widget.AutoCompleteTextView;
  46import android.widget.CheckBox;
  47import android.widget.EditText;
  48import android.widget.ListView;
  49import android.widget.Spinner;
  50import android.widget.TextView;
  51import android.widget.Toast;
  52
  53import java.util.ArrayList;
  54import java.util.Arrays;
  55import java.util.Collections;
  56import java.util.List;
  57import java.util.concurrent.atomic.AtomicBoolean;
  58
  59import eu.siacs.conversations.Config;
  60import eu.siacs.conversations.R;
  61import eu.siacs.conversations.databinding.ActivityStartConversationBinding;
  62import eu.siacs.conversations.entities.Account;
  63import eu.siacs.conversations.entities.Bookmark;
  64import eu.siacs.conversations.entities.Contact;
  65import eu.siacs.conversations.entities.Conversation;
  66import eu.siacs.conversations.entities.ListItem;
  67import eu.siacs.conversations.entities.Presence;
  68import eu.siacs.conversations.services.XmppConnectionService;
  69import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
  70import eu.siacs.conversations.ui.adapter.ListItemAdapter;
  71import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
  72import eu.siacs.conversations.ui.service.EmojiService;
  73import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
  74import eu.siacs.conversations.ui.util.PendingItem;
  75import eu.siacs.conversations.utils.XmppUri;
  76import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
  77import eu.siacs.conversations.xmpp.XmppConnection;
  78import rocks.xmpp.addr.Jid;
  79
  80public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreateConferenceDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener {
  81
  82	private final int REQUEST_SYNC_CONTACTS = 0x28cf;
  83	private final int REQUEST_CREATE_CONFERENCE = 0x39da;
  84	private final PendingItem<Intent> pendingViewIntent = new PendingItem<>();
  85	private final PendingItem<String> mInitialSearchValue = new PendingItem<>();
  86	private final AtomicBoolean oneShotKeyboardSuppress = new AtomicBoolean();
  87	public int conference_context_id;
  88	public int contact_context_id;
  89	private ListPagerAdapter mListPagerAdapter;
  90	private List<ListItem> contacts = new ArrayList<>();
  91	private ListItemAdapter mContactsAdapter;
  92	private List<ListItem> conferences = new ArrayList<>();
  93	private ListItemAdapter mConferenceAdapter;
  94	private List<String> mActivatedAccounts = new ArrayList<>();
  95	private EditText mSearchEditText;
  96	private AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false);
  97	private boolean mHideOfflineContacts = false;
  98	private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
  99
 100		@Override
 101		public boolean onMenuItemActionExpand(MenuItem item) {
 102			mSearchEditText.post(() -> {
 103				mSearchEditText.requestFocus();
 104				if (oneShotKeyboardSuppress.compareAndSet(true, false)) {
 105					return;
 106				}
 107				InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
 108				if (imm != null) {
 109					imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT);
 110				}
 111			});
 112
 113			return true;
 114		}
 115
 116		@Override
 117		public boolean onMenuItemActionCollapse(MenuItem item) {
 118			hideKeyboard();
 119			mSearchEditText.setText("");
 120			filter(null);
 121			return true;
 122		}
 123	};
 124	private TextWatcher mSearchTextWatcher = new TextWatcher() {
 125
 126		@Override
 127		public void afterTextChanged(Editable editable) {
 128			filter(editable.toString());
 129		}
 130
 131		@Override
 132		public void beforeTextChanged(CharSequence s, int start, int count, int after) {
 133		}
 134
 135		@Override
 136		public void onTextChanged(CharSequence s, int start, int before, int count) {
 137		}
 138	};
 139	private MenuItem mMenuSearchView;
 140	private ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() {
 141		@Override
 142		public void onTagClicked(String tag) {
 143			if (mMenuSearchView != null) {
 144				mMenuSearchView.expandActionView();
 145				mSearchEditText.setText("");
 146				mSearchEditText.append(tag);
 147				filter(tag);
 148			}
 149		}
 150	};
 151	private Pair<Integer, Intent> mPostponedActivityResult;
 152	private Toast mToast;
 153	private UiCallback<Conversation> mAdhocConferenceCallback = new UiCallback<Conversation>() {
 154		@Override
 155		public void success(final Conversation conversation) {
 156			runOnUiThread(() -> {
 157				hideToast();
 158				switchToConversation(conversation);
 159			});
 160		}
 161
 162		@Override
 163		public void error(final int errorCode, Conversation object) {
 164			runOnUiThread(() -> replaceToast(getString(errorCode)));
 165		}
 166
 167		@Override
 168		public void userInputRequried(PendingIntent pi, Conversation object) {
 169
 170		}
 171	};
 172	private ActivityStartConversationBinding binding;
 173	private TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() {
 174		@Override
 175		public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
 176			int pos = binding.startConversationViewPager.getCurrentItem();
 177			if (pos == 0) {
 178				if (contacts.size() == 1) {
 179					openConversationForContact((Contact) contacts.get(0));
 180					return true;
 181				}
 182			} else {
 183				if (conferences.size() == 1) {
 184					openConversationsForBookmark((Bookmark) conferences.get(0));
 185					return true;
 186				}
 187			}
 188			hideKeyboard();
 189			mListPagerAdapter.requestFocus(pos);
 190			return true;
 191		}
 192	};
 193	private ViewPager.SimpleOnPageChangeListener mOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() {
 194		@Override
 195		public void onPageSelected(int position) {
 196			onTabChanged();
 197		}
 198	};
 199
 200	public static void populateAccountSpinner(Context context, List<String> accounts, Spinner spinner) {
 201		if (accounts.size() > 0) {
 202			ArrayAdapter<String> adapter = new ArrayAdapter<>(context, R.layout.simple_list_item, accounts);
 203			adapter.setDropDownViewResource(R.layout.simple_list_item);
 204			spinner.setAdapter(adapter);
 205			spinner.setEnabled(true);
 206		} else {
 207			ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
 208					R.layout.simple_list_item,
 209					Arrays.asList(context.getString(R.string.no_accounts)));
 210			adapter.setDropDownViewResource(R.layout.simple_list_item);
 211			spinner.setAdapter(adapter);
 212			spinner.setEnabled(false);
 213		}
 214	}
 215
 216	public static void launch(Context context) {
 217		final Intent intent = new Intent(context, StartConversationActivity.class);
 218		context.startActivity(intent);
 219	}
 220
 221	private static Intent createLauncherIntent(Context context) {
 222		final Intent intent = new Intent(context, StartConversationActivity.class);
 223		intent.setAction(Intent.ACTION_MAIN);
 224		intent.addCategory(Intent.CATEGORY_LAUNCHER);
 225		return intent;
 226	}
 227
 228	private static boolean isViewIntent(final Intent i) {
 229		return i != null && (Intent.ACTION_VIEW.equals(i.getAction()) || Intent.ACTION_SENDTO.equals(i.getAction()) || i.hasExtra(WelcomeActivity.EXTRA_INVITE_URI));
 230	}
 231
 232	protected void hideToast() {
 233		if (mToast != null) {
 234			mToast.cancel();
 235		}
 236	}
 237
 238	protected void replaceToast(String msg) {
 239		hideToast();
 240		mToast = Toast.makeText(this, msg, Toast.LENGTH_LONG);
 241		mToast.show();
 242	}
 243
 244	@Override
 245	public void onRosterUpdate() {
 246		this.refreshUi();
 247	}
 248
 249	@Override
 250	public void onCreate(Bundle savedInstanceState) {
 251		super.onCreate(savedInstanceState);
 252		new EmojiService(this).init();
 253		this.binding = DataBindingUtil.setContentView(this, R.layout.activity_start_conversation);
 254		Toolbar toolbar = (Toolbar) binding.toolbar;
 255		setSupportActionBar(toolbar);
 256		configureActionBar(getSupportActionBar());
 257		this.binding.fab.setOnClickListener((v) -> {
 258			if (binding.startConversationViewPager.getCurrentItem() == 0) {
 259				String searchString = mSearchEditText != null ? mSearchEditText.getText().toString() : null;
 260				if (searchString != null && !searchString.trim().isEmpty()) {
 261					try {
 262						Jid jid = Jid.of(searchString);
 263						if (jid.getLocal() != null && jid.isBareJid() && jid.getDomain().contains(".")) {
 264							showCreateContactDialog(jid.toString(), null);
 265							return;
 266						}
 267					} catch (IllegalArgumentException ignored) {
 268						//ignore and fall through
 269					}
 270				}
 271				showCreateContactDialog(null, null);
 272			} else {
 273				showCreateConferenceDialog();
 274			}
 275		});
 276		binding.tabLayout.setupWithViewPager(binding.startConversationViewPager);
 277		binding.startConversationViewPager.addOnPageChangeListener(mOnPageChangeListener);
 278		mListPagerAdapter = new ListPagerAdapter(getSupportFragmentManager());
 279		binding.startConversationViewPager.setAdapter(mListPagerAdapter);
 280
 281		mConferenceAdapter = new ListItemAdapter(this, conferences);
 282		mContactsAdapter = new ListItemAdapter(this, contacts);
 283		mContactsAdapter.setOnTagClickedListener(this.mOnTagClickedListener);
 284		this.mHideOfflineContacts = getPreferences().getBoolean("hide_offline", false);
 285
 286		final Intent intent;
 287		if (savedInstanceState == null) {
 288			intent = getIntent();
 289		} else {
 290			final String search = savedInstanceState.getString("search");
 291			if (search != null) {
 292				mInitialSearchValue.push(search);
 293			}
 294			intent = savedInstanceState.getParcelable("intent");
 295		}
 296
 297		if (isViewIntent(intent)) {
 298			pendingViewIntent.push(intent);
 299			setIntent(createLauncherIntent(this));
 300		}
 301	}
 302
 303	@Override
 304	public void onSaveInstanceState(Bundle savedInstanceState) {
 305		Intent pendingIntent = pendingViewIntent.peek();
 306		savedInstanceState.putParcelable("intent", pendingIntent != null ? pendingIntent : getIntent());
 307		if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) {
 308			savedInstanceState.putString("search", mSearchEditText != null ? mSearchEditText.getText().toString() : null);
 309		}
 310		super.onSaveInstanceState(savedInstanceState);
 311	}
 312
 313	@Override
 314	public void onStart() {
 315		super.onStart();
 316		final int theme = findTheme();
 317		if (this.mTheme != theme) {
 318			recreate();
 319		} else {
 320			if (pendingViewIntent.peek() == null) {
 321				askForContactsPermissions();
 322			}
 323		}
 324		mConferenceAdapter.refreshSettings();
 325		mContactsAdapter.refreshSettings();
 326	}
 327
 328	@Override
 329	public void onNewIntent(final Intent intent) {
 330		if (xmppConnectionServiceBound) {
 331			processViewIntent(intent);
 332		} else {
 333			pendingViewIntent.push(intent);
 334		}
 335		setIntent(createLauncherIntent(this));
 336	}
 337
 338	protected void openConversationForContact(int position) {
 339		Contact contact = (Contact) contacts.get(position);
 340		openConversationForContact(contact);
 341	}
 342
 343	protected void openConversationForContact(Contact contact) {
 344		Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
 345		switchToConversation(conversation);
 346	}
 347
 348	protected void openConversationForBookmark() {
 349		openConversationForBookmark(conference_context_id);
 350	}
 351
 352	protected void openConversationForBookmark(int position) {
 353		Bookmark bookmark = (Bookmark) conferences.get(position);
 354		openConversationsForBookmark(bookmark);
 355	}
 356
 357	protected void shareBookmarkUri() {
 358		shareBookmarkUri(conference_context_id);
 359	}
 360
 361	protected void shareBookmarkUri(int position) {
 362		Bookmark bookmark = (Bookmark) conferences.get(position);
 363		Intent shareIntent = new Intent();
 364		shareIntent.setAction(Intent.ACTION_SEND);
 365		shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:" + bookmark.getJid().asBareJid().toEscapedString() + "?join");
 366		shareIntent.setType("text/plain");
 367		try {
 368			startActivity(Intent.createChooser(shareIntent, getText(R.string.share_uri_with)));
 369		} catch (ActivityNotFoundException e) {
 370			Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
 371		}
 372	}
 373
 374	protected void openConversationsForBookmark(Bookmark bookmark) {
 375		Jid jid = bookmark.getJid();
 376		if (jid == null) {
 377			Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
 378			return;
 379		}
 380		Conversation conversation = xmppConnectionService.findOrCreateConversation(bookmark.getAccount(), jid, true, true, true);
 381		bookmark.setConversation(conversation);
 382		if (!bookmark.autojoin() && getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin))) {
 383			bookmark.setAutojoin(true);
 384			xmppConnectionService.pushBookmarks(bookmark.getAccount());
 385		}
 386		switchToConversation(conversation);
 387	}
 388
 389	protected void openDetailsForContact() {
 390		int position = contact_context_id;
 391		Contact contact = (Contact) contacts.get(position);
 392		switchToContactDetails(contact);
 393	}
 394
 395	protected void showQrForContact() {
 396		int position = contact_context_id;
 397		Contact contact = (Contact) contacts.get(position);
 398		showQrCode("xmpp:"+contact.getJid().asBareJid().toEscapedString());
 399	}
 400
 401	protected void toggleContactBlock() {
 402		final int position = contact_context_id;
 403		BlockContactDialog.show(this, (Contact) contacts.get(position));
 404	}
 405
 406	protected void deleteContact() {
 407		final int position = contact_context_id;
 408		final Contact contact = (Contact) contacts.get(position);
 409		final AlertDialog.Builder builder = new AlertDialog.Builder(this);
 410		builder.setNegativeButton(R.string.cancel, null);
 411		builder.setTitle(R.string.action_delete_contact);
 412		builder.setMessage(getString(R.string.remove_contact_text, contact.getJid()));
 413		builder.setPositiveButton(R.string.delete, (dialog, which) -> {
 414			xmppConnectionService.deleteContactOnServer(contact);
 415			filter(mSearchEditText.getText().toString());
 416		});
 417		builder.create().show();
 418	}
 419
 420	protected void deleteConference() {
 421		int position = conference_context_id;
 422		final Bookmark bookmark = (Bookmark) conferences.get(position);
 423
 424		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 425		builder.setNegativeButton(R.string.cancel, null);
 426		builder.setTitle(R.string.delete_bookmark);
 427		builder.setMessage(getString(R.string.remove_bookmark_text,
 428				bookmark.getJid()));
 429		builder.setPositiveButton(R.string.delete, (dialog, which) -> {
 430			bookmark.setConversation(null);
 431			Account account = bookmark.getAccount();
 432			account.getBookmarks().remove(bookmark);
 433			xmppConnectionService.pushBookmarks(account);
 434			filter(mSearchEditText.getText().toString());
 435		});
 436		builder.create().show();
 437
 438	}
 439
 440	@SuppressLint("InflateParams")
 441	protected void showCreateContactDialog(final String prefilledJid, final Invite invite) {
 442		FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
 443		Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
 444		if (prev != null) {
 445			ft.remove(prev);
 446		}
 447		ft.addToBackStack(null);
 448		EnterJidDialog dialog = EnterJidDialog.newInstance(
 449				mActivatedAccounts,
 450				getString(R.string.dialog_title_create_contact),
 451				getString(R.string.create),
 452				prefilledJid,
 453				null,
 454				invite == null || !invite.hasFingerprints()
 455		);
 456
 457		dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid) -> {
 458			if (!xmppConnectionServiceBound) {
 459				return false;
 460			}
 461
 462			final Account account = xmppConnectionService.findAccountByJid(accountJid);
 463			if (account == null) {
 464				return true;
 465			}
 466
 467			final Contact contact = account.getRoster().getContact(contactJid);
 468			if (invite != null && invite.getName() != null) {
 469				contact.setServerName(invite.getName());
 470			}
 471			if (contact.isSelf()) {
 472				switchToConversation(contact, null);
 473				return true;
 474			} else if (contact.showInRoster()) {
 475				throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists));
 476			} else {
 477				xmppConnectionService.createContact(contact, true);
 478				if (invite != null && invite.hasFingerprints()) {
 479					xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
 480				}
 481				switchToConversation(contact, invite == null ? null : invite.getBody());
 482				return true;
 483			}
 484		});
 485		dialog.show(ft, FRAGMENT_TAG_DIALOG);
 486	}
 487
 488	@SuppressLint("InflateParams")
 489	protected void showJoinConferenceDialog(final String prefilledJid) {
 490		FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
 491		Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
 492		if (prev != null) {
 493			ft.remove(prev);
 494		}
 495		ft.addToBackStack(null);
 496		JoinConferenceDialog joinConferenceFragment = JoinConferenceDialog.newInstance(prefilledJid, mActivatedAccounts);
 497		joinConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG);
 498	}
 499
 500	private void showCreateConferenceDialog() {
 501		FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
 502		Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
 503		if (prev != null) {
 504			ft.remove(prev);
 505		}
 506		ft.addToBackStack(null);
 507		CreateConferenceDialog createConferenceFragment = CreateConferenceDialog.newInstance(mActivatedAccounts);
 508		createConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG);
 509	}
 510
 511	private Account getSelectedAccount(Spinner spinner) {
 512		if (!spinner.isEnabled()) {
 513			return null;
 514		}
 515		Jid jid;
 516		try {
 517			if (Config.DOMAIN_LOCK != null) {
 518				jid = Jid.of((String) spinner.getSelectedItem(), Config.DOMAIN_LOCK, null);
 519			} else {
 520				jid = Jid.of((String) spinner.getSelectedItem());
 521			}
 522		} catch (final IllegalArgumentException e) {
 523			return null;
 524		}
 525		return xmppConnectionService.findAccountByJid(jid);
 526	}
 527
 528	protected void switchToConversation(Contact contact, String body) {
 529		Conversation conversation = xmppConnectionService
 530				.findOrCreateConversation(contact.getAccount(),
 531						contact.getJid(), false, true);
 532		switchToConversation(conversation, body, false);
 533	}
 534
 535	@Override
 536	public void invalidateOptionsMenu() {
 537		boolean isExpanded = mMenuSearchView != null && mMenuSearchView.isActionViewExpanded();
 538		String text = mSearchEditText != null ? mSearchEditText.getText().toString() : "";
 539		if (isExpanded) {
 540			mInitialSearchValue.push(text);
 541			oneShotKeyboardSuppress.set(true);
 542		}
 543		super.invalidateOptionsMenu();
 544	}
 545
 546	@Override
 547	public boolean onCreateOptionsMenu(Menu menu) {
 548		getMenuInflater().inflate(R.menu.start_conversation, menu);
 549		MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline);
 550		MenuItem joinGroupChat = menu.findItem(R.id.action_join_conference);
 551		MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
 552		joinGroupChat.setVisible(binding.startConversationViewPager.getCurrentItem() == 1);
 553		qrCodeScanMenuItem.setVisible(isCameraFeatureAvailable());
 554		menuHideOffline.setChecked(this.mHideOfflineContacts);
 555		mMenuSearchView = menu.findItem(R.id.action_search);
 556		mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener);
 557		View mSearchView = mMenuSearchView.getActionView();
 558		mSearchEditText = mSearchView.findViewById(R.id.search_field);
 559		mSearchEditText.addTextChangedListener(mSearchTextWatcher);
 560		mSearchEditText.setOnEditorActionListener(mSearchDone);
 561		String initialSearchValue = mInitialSearchValue.pop();
 562		if (initialSearchValue != null) {
 563			mMenuSearchView.expandActionView();
 564			mSearchEditText.append(initialSearchValue);
 565			filter(initialSearchValue);
 566		}
 567		return super.onCreateOptionsMenu(menu);
 568	}
 569
 570	@Override
 571	public boolean onOptionsItemSelected(MenuItem item) {
 572		if (MenuDoubleTabUtil.shouldIgnoreTap()) {
 573			return false;
 574		}
 575		switch (item.getItemId()) {
 576			case android.R.id.home:
 577				navigateBack();
 578				return true;
 579			case R.id.action_join_conference:
 580				showJoinConferenceDialog(null);
 581				return true;
 582			case R.id.action_scan_qr_code:
 583				UriHandlerActivity.scan(this);
 584				return true;
 585			case R.id.action_hide_offline:
 586				mHideOfflineContacts = !item.isChecked();
 587				getPreferences().edit().putBoolean("hide_offline", mHideOfflineContacts).commit();
 588				if (mSearchEditText != null) {
 589					filter(mSearchEditText.getText().toString());
 590				}
 591				invalidateOptionsMenu();
 592		}
 593		return super.onOptionsItemSelected(item);
 594	}
 595
 596	@Override
 597	public boolean onKeyUp(int keyCode, KeyEvent event) {
 598		if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) {
 599			openSearch();
 600			return true;
 601		}
 602		int c = event.getUnicodeChar();
 603		if (c > 32) {
 604			if (mSearchEditText != null && !mSearchEditText.isFocused()) {
 605				openSearch();
 606				mSearchEditText.append(Character.toString((char) c));
 607				return true;
 608			}
 609		}
 610		return super.onKeyUp(keyCode, event);
 611	}
 612
 613	private void openSearch() {
 614		if (mMenuSearchView != null) {
 615			mMenuSearchView.expandActionView();
 616		}
 617	}
 618
 619	@Override
 620	public void onActivityResult(int requestCode, int resultCode, Intent intent) {
 621		if (resultCode == RESULT_OK) {
 622			if (xmppConnectionServiceBound) {
 623				this.mPostponedActivityResult = null;
 624				if (requestCode == REQUEST_CREATE_CONFERENCE) {
 625					Account account = extractAccount(intent);
 626					final String subject = intent.getStringExtra("subject");
 627					List<Jid> jids = new ArrayList<>();
 628					if (intent.getBooleanExtra("multiple", false)) {
 629						String[] toAdd = intent.getStringArrayExtra("contacts");
 630						for (String item : toAdd) {
 631							try {
 632								jids.add(Jid.of(item));
 633							} catch (IllegalArgumentException e) {
 634								//ignored
 635							}
 636						}
 637					} else {
 638						try {
 639							jids.add(Jid.of(intent.getStringExtra("contact")));
 640						} catch (Exception e) {
 641							//ignored
 642						}
 643					}
 644					if (account != null && jids.size() > 0) {
 645						if (xmppConnectionService.createAdhocConference(account, subject, jids, mAdhocConferenceCallback)) {
 646							mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
 647							mToast.show();
 648						}
 649					}
 650				}
 651			} else {
 652				this.mPostponedActivityResult = new Pair<>(requestCode, intent);
 653			}
 654		}
 655		super.onActivityResult(requestCode, requestCode, intent);
 656	}
 657
 658	private void askForContactsPermissions() {
 659		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 660			if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
 661				if (mRequestedContactsPermission.compareAndSet(false, true)) {
 662					if (shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
 663						AlertDialog.Builder builder = new AlertDialog.Builder(this);
 664						builder.setTitle(R.string.sync_with_contacts);
 665						builder.setMessage(R.string.sync_with_contacts_long);
 666						builder.setPositiveButton(R.string.next, (dialog, which) -> {
 667							if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 668								requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
 669							}
 670						});
 671						builder.setOnDismissListener(dialog -> {
 672							if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 673								requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
 674							}
 675						});
 676						builder.create().show();
 677					} else {
 678						requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, 0);
 679					}
 680				}
 681			}
 682		}
 683	}
 684
 685	@Override
 686	public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) {
 687		if (grantResults.length > 0)
 688			if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 689				ScanActivity.onRequestPermissionResult(this, requestCode, grantResults);
 690				if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
 691					xmppConnectionService.loadPhoneContacts();
 692				}
 693			}
 694	}
 695
 696	private void configureHomeButton() {
 697		final ActionBar actionBar = getSupportActionBar();
 698		if (actionBar == null) {
 699			return;
 700		}
 701		boolean openConversations = !xmppConnectionService.isConversationsListEmpty(null);
 702		actionBar.setDisplayHomeAsUpEnabled(openConversations);
 703		actionBar.setDisplayHomeAsUpEnabled(openConversations);
 704
 705	}
 706
 707	@Override
 708	protected void onBackendConnected() {
 709		if (mPostponedActivityResult != null) {
 710			onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
 711			this.mPostponedActivityResult = null;
 712		}
 713		this.mActivatedAccounts.clear();
 714		for (Account account : xmppConnectionService.getAccounts()) {
 715			if (account.getStatus() != Account.State.DISABLED) {
 716				if (Config.DOMAIN_LOCK != null) {
 717					this.mActivatedAccounts.add(account.getJid().getLocal());
 718				} else {
 719					this.mActivatedAccounts.add(account.getJid().asBareJid().toString());
 720				}
 721			}
 722		}
 723		configureHomeButton();
 724		Intent intent = pendingViewIntent.pop();
 725		if (intent != null && processViewIntent(intent)) {
 726			filter(null);
 727		} else {
 728			if (mSearchEditText != null) {
 729				filter(mSearchEditText.getText().toString());
 730			} else {
 731				filter(null);
 732			}
 733		}
 734		Fragment fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
 735		if (fragment != null && fragment instanceof OnBackendConnected) {
 736			Log.d(Config.LOGTAG, "calling on backend connected on dialog");
 737			((OnBackendConnected) fragment).onBackendConnected();
 738		}
 739	}
 740
 741	protected boolean processViewIntent(@NonNull Intent intent) {
 742		final String inviteUri = intent.getStringExtra(WelcomeActivity.EXTRA_INVITE_URI);
 743		if (inviteUri != null) {
 744			Invite invite = new Invite(inviteUri);
 745			if (invite.isJidValid()) {
 746				return invite.invite();
 747			}
 748		}
 749		final String action = intent.getAction();
 750		if (action == null) {
 751			return false;
 752		}
 753		switch (action) {
 754			case Intent.ACTION_SENDTO:
 755			case Intent.ACTION_VIEW:
 756				Uri uri = intent.getData();
 757				if (uri != null) {
 758					Invite invite = new Invite(intent.getData(), intent.getBooleanExtra("scanned", false));
 759					invite.account = intent.getStringExtra("account");
 760					return invite.invite();
 761				} else {
 762					return false;
 763				}
 764		}
 765		return false;
 766	}
 767
 768	private boolean handleJid(Invite invite) {
 769		List<Contact> contacts = xmppConnectionService.findContacts(invite.getJid(), invite.account);
 770		if (invite.isAction(XmppUri.ACTION_JOIN)) {
 771			Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid());
 772			if (muc != null) {
 773				switchToConversation(muc, invite.getBody(), false);
 774				return true;
 775			} else {
 776				showJoinConferenceDialog(invite.getJid().asBareJid().toString());
 777				return false;
 778			}
 779		} else if (contacts.size() == 0) {
 780			showCreateContactDialog(invite.getJid().toString(), invite);
 781			return false;
 782		} else if (contacts.size() == 1) {
 783			Contact contact = contacts.get(0);
 784			if (!invite.isSafeSource() && invite.hasFingerprints()) {
 785				displayVerificationWarningDialog(contact, invite);
 786			} else {
 787				if (invite.hasFingerprints()) {
 788					if (xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints())) {
 789						Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
 790					}
 791				}
 792				if (invite.account != null) {
 793					xmppConnectionService.getShortcutService().report(contact);
 794				}
 795				switchToConversation(contact, invite.getBody());
 796			}
 797			return true;
 798		} else {
 799			if (mMenuSearchView != null) {
 800				mMenuSearchView.expandActionView();
 801				mSearchEditText.setText("");
 802				mSearchEditText.append(invite.getJid().toString());
 803				filter(invite.getJid().toString());
 804			} else {
 805				mInitialSearchValue.push(invite.getJid().toString());
 806			}
 807			return true;
 808		}
 809	}
 810
 811	private void displayVerificationWarningDialog(final Contact contact, final Invite invite) {
 812		AlertDialog.Builder builder = new AlertDialog.Builder(this);
 813		builder.setTitle(R.string.verify_omemo_keys);
 814		View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
 815		final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source);
 816		TextView warning = view.findViewById(R.id.warning);
 817		String jid = contact.getJid().asBareJid().toString();
 818		SpannableString spannable = new SpannableString(getString(R.string.verifying_omemo_keys_trusted_source, jid, contact.getDisplayName()));
 819		int start = spannable.toString().indexOf(jid);
 820		if (start >= 0) {
 821			spannable.setSpan(new TypefaceSpan("monospace"), start, start + jid.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
 822		}
 823		warning.setText(spannable);
 824		builder.setView(view);
 825		builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
 826			if (isTrustedSource.isChecked() && invite.hasFingerprints()) {
 827				xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
 828			}
 829			switchToConversation(contact, invite.getBody());
 830		});
 831		builder.setNegativeButton(R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish());
 832		AlertDialog dialog = builder.create();
 833		dialog.setCanceledOnTouchOutside(false);
 834		dialog.setOnCancelListener(dialog1 -> StartConversationActivity.this.finish());
 835		dialog.show();
 836	}
 837
 838	protected void filter(String needle) {
 839		if (xmppConnectionServiceBound) {
 840			this.filterContacts(needle);
 841			this.filterConferences(needle);
 842		}
 843	}
 844
 845	protected void filterContacts(String needle) {
 846		this.contacts.clear();
 847		for (Account account : xmppConnectionService.getAccounts()) {
 848			if (account.getStatus() != Account.State.DISABLED) {
 849				for (Contact contact : account.getRoster().getContacts()) {
 850					Presence.Status s = contact.getShownStatus();
 851					if (contact.showInRoster() && contact.match(this, needle)
 852							&& (!this.mHideOfflineContacts
 853							|| (needle != null && !needle.trim().isEmpty())
 854							|| s.compareTo(Presence.Status.OFFLINE) < 0)) {
 855						this.contacts.add(contact);
 856					}
 857				}
 858			}
 859		}
 860		Collections.sort(this.contacts);
 861		mContactsAdapter.notifyDataSetChanged();
 862	}
 863
 864	protected void filterConferences(String needle) {
 865		this.conferences.clear();
 866		for (Account account : xmppConnectionService.getAccounts()) {
 867			if (account.getStatus() != Account.State.DISABLED) {
 868				for (Bookmark bookmark : account.getBookmarks()) {
 869					if (bookmark.match(this, needle)) {
 870						this.conferences.add(bookmark);
 871					}
 872				}
 873			}
 874		}
 875		Collections.sort(this.conferences);
 876		mConferenceAdapter.notifyDataSetChanged();
 877	}
 878
 879	private void onTabChanged() {
 880		@DrawableRes final int fabDrawable;
 881		if (binding.startConversationViewPager.getCurrentItem() == 0) {
 882			fabDrawable = R.drawable.ic_person_add_white_24dp;
 883		} else {
 884			fabDrawable = R.drawable.ic_group_add_white_24dp;
 885		}
 886		binding.fab.setImageResource(fabDrawable);
 887		invalidateOptionsMenu();
 888	}
 889
 890	@Override
 891	public void OnUpdateBlocklist(final Status status) {
 892		refreshUi();
 893	}
 894
 895	@Override
 896	protected void refreshUiReal() {
 897		if (mSearchEditText != null) {
 898			filter(mSearchEditText.getText().toString());
 899		}
 900		configureHomeButton();
 901	}
 902
 903	@Override
 904	public void onBackPressed() {
 905		navigateBack();
 906	}
 907
 908	private void navigateBack() {
 909		if (xmppConnectionService != null && !xmppConnectionService.isConversationsListEmpty(null)) {
 910			Intent intent = new Intent(this, ConversationsActivity.class);
 911			intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
 912			startActivity(intent);
 913		}
 914		finish();
 915	}
 916
 917	@Override
 918	public void onCreateDialogPositiveClick(Spinner spinner, String subject) {
 919		if (!xmppConnectionServiceBound) {
 920			return;
 921		}
 922		final Account account = getSelectedAccount(spinner);
 923		if (account == null) {
 924			return;
 925		}
 926		Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class);
 927		intent.putExtra("multiple", true);
 928		intent.putExtra("show_enter_jid", true);
 929		intent.putExtra("subject", subject);
 930		intent.putExtra(EXTRA_ACCOUNT, account.getJid().asBareJid().toString());
 931		intent.putExtra(ChooseContactActivity.EXTRA_TITLE_RES_ID, R.string.choose_participants);
 932		startActivityForResult(intent, REQUEST_CREATE_CONFERENCE);
 933	}
 934
 935	@Override
 936	public void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, AutoCompleteTextView jid, boolean isBookmarkChecked) {
 937		if (!xmppConnectionServiceBound) {
 938			return;
 939		}
 940		final Account account = getSelectedAccount(spinner);
 941		if (account == null) {
 942			return;
 943		}
 944		final Jid conferenceJid;
 945		try {
 946			conferenceJid = Jid.of(jid.getText().toString());
 947		} catch (final IllegalArgumentException e) {
 948			jid.setError(getString(R.string.invalid_jid));
 949			return;
 950		}
 951
 952		if (isBookmarkChecked) {
 953			if (account.hasBookmarkFor(conferenceJid)) {
 954				jid.setError(getString(R.string.bookmark_already_exists));
 955			} else {
 956				final Bookmark bookmark = new Bookmark(account, conferenceJid.asBareJid());
 957				bookmark.setAutojoin(getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin)));
 958				String nick = conferenceJid.getResource();
 959				if (nick != null && !nick.isEmpty()) {
 960					bookmark.setNick(nick);
 961				}
 962				account.getBookmarks().add(bookmark);
 963				xmppConnectionService.pushBookmarks(account);
 964				final Conversation conversation = xmppConnectionService
 965						.findOrCreateConversation(account, conferenceJid, true, true, true);
 966				bookmark.setConversation(conversation);
 967				dialog.dismiss();
 968				switchToConversation(conversation);
 969			}
 970		} else {
 971			final Conversation conversation = xmppConnectionService
 972					.findOrCreateConversation(account, conferenceJid, true, true, true);
 973			dialog.dismiss();
 974			switchToConversation(conversation);
 975		}
 976	}
 977
 978	@Override
 979	public void onConversationUpdate() {
 980		refreshUi();
 981	}
 982
 983	public static class MyListFragment extends ListFragment {
 984		private AdapterView.OnItemClickListener mOnItemClickListener;
 985		private int mResContextMenu;
 986
 987		public void setContextMenu(final int res) {
 988			this.mResContextMenu = res;
 989		}
 990
 991		@Override
 992		public void onListItemClick(final ListView l, final View v, final int position, final long id) {
 993			if (mOnItemClickListener != null) {
 994				mOnItemClickListener.onItemClick(l, v, position, id);
 995			}
 996		}
 997
 998		public void setOnListItemClickListener(AdapterView.OnItemClickListener l) {
 999			this.mOnItemClickListener = l;
1000		}
1001
1002		@Override
1003		public void onViewCreated(@NonNull final View view, final Bundle savedInstanceState) {
1004			super.onViewCreated(view, savedInstanceState);
1005			registerForContextMenu(getListView());
1006			getListView().setFastScrollEnabled(true);
1007			getListView().setDivider(null);
1008			getListView().setDividerHeight(0);
1009		}
1010
1011		@Override
1012		public void onCreateContextMenu(final ContextMenu menu, final View v,
1013		                                final ContextMenuInfo menuInfo) {
1014			super.onCreateContextMenu(menu, v, menuInfo);
1015			final StartConversationActivity activity = (StartConversationActivity) getActivity();
1016			if (activity == null) {
1017				return;
1018			}
1019			activity.getMenuInflater().inflate(mResContextMenu, menu);
1020			final AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
1021			if (mResContextMenu == R.menu.conference_context) {
1022				activity.conference_context_id = acmi.position;
1023			} else if (mResContextMenu == R.menu.contact_context) {
1024				activity.contact_context_id = acmi.position;
1025				final Contact contact = (Contact) activity.contacts.get(acmi.position);
1026				final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock);
1027				final MenuItem showContactDetailsItem = menu.findItem(R.id.context_contact_details);
1028				if (contact.isSelf()) {
1029					showContactDetailsItem.setVisible(false);
1030				}
1031				XmppConnection xmpp = contact.getAccount().getXmppConnection();
1032				if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) {
1033					if (contact.isBlocked()) {
1034						blockUnblockItem.setTitle(R.string.unblock_contact);
1035					} else {
1036						blockUnblockItem.setTitle(R.string.block_contact);
1037					}
1038				} else {
1039					blockUnblockItem.setVisible(false);
1040				}
1041			}
1042		}
1043
1044		@Override
1045		public boolean onContextItemSelected(final MenuItem item) {
1046			StartConversationActivity activity = (StartConversationActivity) getActivity();
1047			if (activity == null) {
1048				return true;
1049			}
1050			switch (item.getItemId()) {
1051				case R.id.context_contact_details:
1052					activity.openDetailsForContact();
1053					break;
1054				case R.id.context_show_qr:
1055					activity.showQrForContact();
1056					break;
1057				case R.id.context_contact_block_unblock:
1058					activity.toggleContactBlock();
1059					break;
1060				case R.id.context_delete_contact:
1061					activity.deleteContact();
1062					break;
1063				case R.id.context_join_conference:
1064					activity.openConversationForBookmark();
1065					break;
1066				case R.id.context_share_uri:
1067					activity.shareBookmarkUri();
1068					break;
1069				case R.id.context_delete_conference:
1070					activity.deleteConference();
1071			}
1072			return true;
1073		}
1074	}
1075
1076	public class ListPagerAdapter extends PagerAdapter {
1077		FragmentManager fragmentManager;
1078		MyListFragment[] fragments;
1079
1080		public ListPagerAdapter(FragmentManager fm) {
1081			fragmentManager = fm;
1082			fragments = new MyListFragment[2];
1083		}
1084
1085		public void requestFocus(int pos) {
1086			if (fragments.length > pos) {
1087				fragments[pos].getListView().requestFocus();
1088			}
1089		}
1090
1091		@Override
1092		public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
1093			assert (0 <= position && position < fragments.length);
1094			FragmentTransaction trans = fragmentManager.beginTransaction();
1095			trans.remove(fragments[position]);
1096			trans.commit();
1097			fragments[position] = null;
1098		}
1099
1100		@Override
1101		public Fragment instantiateItem(@NonNull ViewGroup container, int position) {
1102			Fragment fragment = getItem(position);
1103			FragmentTransaction trans = fragmentManager.beginTransaction();
1104			trans.add(container.getId(), fragment, "fragment:" + position);
1105			trans.commit();
1106			return fragment;
1107		}
1108
1109		@Override
1110		public int getCount() {
1111			return fragments.length;
1112		}
1113
1114		@Override
1115		public boolean isViewFromObject(@NonNull View view, @NonNull Object fragment) {
1116			return ((Fragment) fragment).getView() == view;
1117		}
1118
1119		@Nullable
1120		@Override
1121		public CharSequence getPageTitle(int position) {
1122			switch (position) {
1123				case 0:
1124					return getResources().getString(R.string.contacts);
1125				case 1:
1126					return getResources().getString(R.string.conferences);
1127				default:
1128					return super.getPageTitle(position);
1129			}
1130		}
1131
1132		public Fragment getItem(int position) {
1133			assert (0 <= position && position < fragments.length);
1134			if (fragments[position] == null) {
1135				final MyListFragment listFragment = new MyListFragment();
1136				if (position == 1) {
1137					listFragment.setListAdapter(mConferenceAdapter);
1138					listFragment.setContextMenu(R.menu.conference_context);
1139					listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForBookmark(p));
1140				} else {
1141
1142					listFragment.setListAdapter(mContactsAdapter);
1143					listFragment.setContextMenu(R.menu.contact_context);
1144					listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForContact(p));
1145				}
1146				fragments[position] = listFragment;
1147			}
1148			return fragments[position];
1149		}
1150	}
1151
1152	private class Invite extends XmppUri {
1153
1154		public String account;
1155
1156		public Invite(final Uri uri) {
1157			super(uri);
1158		}
1159
1160		public Invite(final String uri) {
1161			super(uri);
1162		}
1163
1164		public Invite(Uri uri, boolean safeSource) {
1165			super(uri, safeSource);
1166		}
1167
1168		boolean invite() {
1169			if (!isJidValid()) {
1170				Toast.makeText(StartConversationActivity.this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
1171				return false;
1172			}
1173			if (getJid() != null) {
1174				return handleJid(this);
1175			}
1176			return false;
1177		}
1178	}
1179}