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.SharedPreferences;
  11import android.content.pm.PackageManager;
  12import android.net.Uri;
  13import android.os.Build;
  14import android.os.Bundle;
  15import android.preference.PreferenceManager;
  16import android.text.Editable;
  17import android.text.Html;
  18import android.text.TextWatcher;
  19import android.text.method.LinkMovementMethod;
  20import android.util.Log;
  21import android.util.Pair;
  22import android.view.ContextMenu;
  23import android.view.ContextMenu.ContextMenuInfo;
  24import android.view.KeyEvent;
  25import android.view.LayoutInflater;
  26import android.view.Menu;
  27import android.view.MenuItem;
  28import android.view.View;
  29import android.view.ViewGroup;
  30import android.view.inputmethod.InputMethodManager;
  31import android.widget.AdapterView;
  32import android.widget.AdapterView.AdapterContextMenuInfo;
  33import android.widget.ArrayAdapter;
  34import android.widget.AutoCompleteTextView;
  35import android.widget.CheckBox;
  36import android.widget.EditText;
  37import android.widget.ListView;
  38import android.widget.Spinner;
  39import android.widget.TextView;
  40import android.widget.Toast;
  41
  42import androidx.annotation.MenuRes;
  43import androidx.annotation.NonNull;
  44import androidx.annotation.Nullable;
  45import androidx.annotation.StringRes;
  46import androidx.appcompat.app.ActionBar;
  47import androidx.appcompat.app.AlertDialog;
  48import androidx.appcompat.widget.PopupMenu;
  49import androidx.core.content.ContextCompat;
  50import androidx.databinding.DataBindingUtil;
  51import androidx.fragment.app.Fragment;
  52import androidx.fragment.app.FragmentManager;
  53import androidx.fragment.app.FragmentTransaction;
  54import androidx.recyclerview.widget.RecyclerView;
  55import androidx.recyclerview.widget.LinearLayoutManager;
  56import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
  57import androidx.viewpager.widget.PagerAdapter;
  58import androidx.viewpager.widget.ViewPager;
  59
  60import com.cheogram.android.FinishOnboarding;
  61
  62import com.google.android.material.textfield.TextInputLayout;
  63import com.leinardi.android.speeddial.SpeedDialActionItem;
  64import com.leinardi.android.speeddial.SpeedDialView;
  65
  66import java.util.Arrays;
  67import java.util.ArrayList;
  68import java.util.Collections;
  69import java.util.Comparator;
  70import java.util.HashSet;
  71import java.util.List;
  72import java.util.Locale;
  73import java.util.Map;
  74import java.util.concurrent.atomic.AtomicBoolean;
  75import java.util.stream.Collectors;
  76
  77import eu.siacs.conversations.Config;
  78import eu.siacs.conversations.R;
  79import eu.siacs.conversations.databinding.ActivityStartConversationBinding;
  80import eu.siacs.conversations.entities.Account;
  81import eu.siacs.conversations.entities.Bookmark;
  82import eu.siacs.conversations.entities.Contact;
  83import eu.siacs.conversations.entities.Conversation;
  84import eu.siacs.conversations.entities.ListItem;
  85import eu.siacs.conversations.entities.MucOptions;
  86import eu.siacs.conversations.entities.Presence;
  87import eu.siacs.conversations.services.QuickConversationsService;
  88import eu.siacs.conversations.services.XmppConnectionService;
  89import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
  90import eu.siacs.conversations.ui.adapter.ListItemAdapter;
  91import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
  92import eu.siacs.conversations.ui.util.JidDialog;
  93import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
  94import eu.siacs.conversations.ui.util.PendingItem;
  95import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
  96import eu.siacs.conversations.ui.widget.SwipeRefreshListFragment;
  97import eu.siacs.conversations.utils.AccountUtils;
  98import eu.siacs.conversations.utils.UIHelper;
  99import eu.siacs.conversations.utils.XmppUri;
 100import eu.siacs.conversations.xml.Element;
 101import eu.siacs.conversations.xml.Namespace;
 102import eu.siacs.conversations.xmpp.Jid;
 103import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 104import eu.siacs.conversations.xmpp.XmppConnection;
 105import eu.siacs.conversations.xmpp.forms.Data;
 106import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 107
 108public class StartConversationActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, CreatePrivateGroupChatDialog.CreateConferenceDialogListener, JoinConferenceDialog.JoinConferenceDialogListener, SwipeRefreshLayout.OnRefreshListener, CreatePublicChannelDialog.CreatePublicChannelDialogListener {
 109
 110    public static final String EXTRA_INVITE_URI = "eu.siacs.conversations.invite_uri";
 111
 112    private final int REQUEST_SYNC_CONTACTS = 0x28cf;
 113    private final int REQUEST_CREATE_CONFERENCE = 0x39da;
 114    private final PendingItem<Intent> pendingViewIntent = new PendingItem<>();
 115    private final PendingItem<String> mInitialSearchValue = new PendingItem<>();
 116    private final AtomicBoolean oneShotKeyboardSuppress = new AtomicBoolean();
 117    public ListItem contextItem;
 118    private ListPagerAdapter mListPagerAdapter;
 119    private final List<ListItem> contacts = new ArrayList<>();
 120    private ListItemAdapter mContactsAdapter;
 121    private TagsAdapter mTagsAdapter = new TagsAdapter();
 122    private final List<ListItem> conferences = new ArrayList<>();
 123    private ListItemAdapter mConferenceAdapter;
 124    private final List<String> mActivatedAccounts = new ArrayList<>();
 125    private EditText mSearchEditText;
 126    private final AtomicBoolean mRequestedContactsPermission = new AtomicBoolean(false);
 127    private final AtomicBoolean mOpenedFab = new AtomicBoolean(false);
 128    private boolean mHideOfflineContacts = false;
 129    private boolean createdByViewIntent = false;
 130    private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() {
 131
 132        @Override
 133        public boolean onMenuItemActionExpand(MenuItem item) {
 134            mSearchEditText.post(() -> {
 135                updateSearchViewHint();
 136                mSearchEditText.requestFocus();
 137                if (oneShotKeyboardSuppress.compareAndSet(true, false)) {
 138                    return;
 139                }
 140                InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
 141                if (imm != null) {
 142                    imm.showSoftInput(mSearchEditText, InputMethodManager.SHOW_IMPLICIT);
 143                }
 144            });
 145            if (binding.speedDial.isOpen()) {
 146                binding.speedDial.close();
 147            }
 148            return true;
 149        }
 150
 151        @Override
 152        public boolean onMenuItemActionCollapse(MenuItem item) {
 153            SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this);
 154            mSearchEditText.setText("");
 155            filter(null);
 156            navigateBack();
 157            return true;
 158        }
 159    };
 160    private final TextWatcher mSearchTextWatcher = new TextWatcher() {
 161
 162        @Override
 163        public void afterTextChanged(Editable editable) {
 164            filter(editable.toString());
 165        }
 166
 167        @Override
 168        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
 169        }
 170
 171        @Override
 172        public void onTextChanged(CharSequence s, int start, int before, int count) {
 173        }
 174    };
 175    private MenuItem mMenuSearchView;
 176    private final ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() {
 177        @Override
 178        public void onTagClicked(String tag) {
 179            if (mMenuSearchView != null) {
 180                mMenuSearchView.expandActionView();
 181                mSearchEditText.setText("");
 182                mSearchEditText.append(tag);
 183                filter(tag);
 184            }
 185        }
 186    };
 187    private Pair<Integer, Intent> mPostponedActivityResult;
 188    private Toast mToast;
 189    private final UiCallback<Conversation> mAdhocConferenceCallback = new UiCallback<Conversation>() {
 190        @Override
 191        public void success(final Conversation conversation) {
 192            runOnUiThread(() -> {
 193                hideToast();
 194                switchToConversation(conversation);
 195            });
 196        }
 197
 198        @Override
 199        public void error(final int errorCode, Conversation object) {
 200            runOnUiThread(() -> replaceToast(getString(errorCode)));
 201        }
 202
 203        @Override
 204        public void userInputRequired(PendingIntent pi, Conversation object) {
 205
 206        }
 207    };
 208    private ActivityStartConversationBinding binding;
 209    private final TextView.OnEditorActionListener mSearchDone = new TextView.OnEditorActionListener() {
 210        @Override
 211        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
 212            int pos = binding.startConversationViewPager.getCurrentItem();
 213            if (pos == 0) {
 214                if (contacts.size() == 1) {
 215                    openConversation(contacts.get(0));
 216                    return true;
 217                } else if (contacts.size() == 0 && conferences.size() == 1) {
 218                    openConversationsForBookmark((Bookmark) conferences.get(0));
 219                    return true;
 220                }
 221            } else {
 222                if (conferences.size() == 1) {
 223                    openConversationsForBookmark((Bookmark) conferences.get(0));
 224                    return true;
 225                } else if (conferences.size() == 0 && contacts.size() == 1) {
 226                    openConversation(contacts.get(0));
 227                    return true;
 228                }
 229            }
 230            SoftKeyboardUtils.hideSoftKeyboard(StartConversationActivity.this);
 231            mListPagerAdapter.requestFocus(pos);
 232            return true;
 233        }
 234    };
 235
 236    public static void populateAccountSpinner(Context context, List<String> accounts, Spinner spinner) {
 237        if (accounts.size() > 0) {
 238            ArrayAdapter<String> adapter = new ArrayAdapter<>(context, R.layout.simple_list_item, accounts);
 239            adapter.setDropDownViewResource(R.layout.simple_list_item);
 240            spinner.setAdapter(adapter);
 241            spinner.setEnabled(true);
 242        } else {
 243            ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
 244                    R.layout.simple_list_item,
 245                    Collections.singletonList(context.getString(R.string.no_accounts)));
 246            adapter.setDropDownViewResource(R.layout.simple_list_item);
 247            spinner.setAdapter(adapter);
 248            spinner.setEnabled(false);
 249        }
 250    }
 251
 252    public static void launch(Context context) {
 253        final Intent intent = new Intent(context, StartConversationActivity.class);
 254        context.startActivity(intent);
 255    }
 256
 257    private static Intent createLauncherIntent(Context context) {
 258        final Intent intent = new Intent(context, StartConversationActivity.class);
 259        intent.setAction(Intent.ACTION_MAIN);
 260        intent.addCategory(Intent.CATEGORY_LAUNCHER);
 261        return intent;
 262    }
 263
 264    private static boolean isViewIntent(final Intent i) {
 265        return i != null && (Intent.ACTION_VIEW.equals(i.getAction()) || Intent.ACTION_SENDTO.equals(i.getAction()) || i.hasExtra(EXTRA_INVITE_URI));
 266    }
 267
 268    protected void hideToast() {
 269        if (mToast != null) {
 270            mToast.cancel();
 271        }
 272    }
 273
 274    protected void replaceToast(String msg) {
 275        hideToast();
 276        mToast = Toast.makeText(this, msg, Toast.LENGTH_LONG);
 277        mToast.show();
 278    }
 279
 280    @Override
 281    public void onRosterUpdate() {
 282        this.refreshUi();
 283    }
 284
 285    @Override
 286    public void onCreate(Bundle savedInstanceState) {
 287        super.onCreate(savedInstanceState);
 288        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_start_conversation);
 289        setSupportActionBar(binding.toolbar);
 290        configureActionBar(getSupportActionBar());
 291
 292        inflateFab(binding.speedDial, R.menu.start_conversation_fab_submenu);
 293        binding.tabLayout.setupWithViewPager(binding.startConversationViewPager);
 294        binding.startConversationViewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
 295            @Override
 296            public void onPageSelected(int position) {
 297                updateSearchViewHint();
 298            }
 299        });
 300        mListPagerAdapter = new ListPagerAdapter(getSupportFragmentManager());
 301        binding.startConversationViewPager.setAdapter(mListPagerAdapter);
 302
 303        mConferenceAdapter = new ListItemAdapter(this, conferences);
 304        mContactsAdapter = new ListItemAdapter(this, contacts);
 305        mContactsAdapter.setOnTagClickedListener(this.mOnTagClickedListener);
 306
 307        final SharedPreferences preferences = getPreferences();
 308
 309        this.mHideOfflineContacts = QuickConversationsService.isConversations() && preferences.getBoolean("hide_offline", false);
 310
 311        final boolean startSearching = preferences.getBoolean("start_searching", getResources().getBoolean(R.bool.start_searching));
 312
 313        final Intent intent;
 314        if (savedInstanceState == null) {
 315            intent = getIntent();
 316        } else {
 317            createdByViewIntent = savedInstanceState.getBoolean("created_by_view_intent", false);
 318            final String search = savedInstanceState.getString("search");
 319            if (search != null) {
 320                mInitialSearchValue.push(search);
 321            }
 322            intent = savedInstanceState.getParcelable("intent");
 323        }
 324
 325        if (intent.getBooleanExtra("init", false)) {
 326            pendingViewIntent.push(intent);
 327        }
 328
 329        if (isViewIntent(intent)) {
 330            pendingViewIntent.push(intent);
 331            createdByViewIntent = true;
 332            setIntent(createLauncherIntent(this));
 333        } else if (startSearching && mInitialSearchValue.peek() == null) {
 334            mInitialSearchValue.push("");
 335        }
 336        mRequestedContactsPermission.set(savedInstanceState != null && savedInstanceState.getBoolean("requested_contacts_permission", false));
 337        mOpenedFab.set(savedInstanceState != null && savedInstanceState.getBoolean("opened_fab", false));
 338        binding.speedDial.setOnActionSelectedListener(actionItem -> {
 339            final String searchString = mSearchEditText != null ? mSearchEditText.getText().toString() : null;
 340            final String prefilled;
 341            if (isValidJid(searchString)) {
 342                prefilled = Jid.ofEscaped(searchString).toEscapedString();
 343            } else {
 344                prefilled = null;
 345            }
 346            switch (actionItem.getId()) {
 347                case R.id.discover_public_channels:
 348                    if (QuickConversationsService.isPlayStoreFlavor()) {
 349                        throw new IllegalStateException("Channel discovery is not available on Google Play flavor");
 350                    } else {
 351                        startActivity(new Intent(this, ChannelDiscoveryActivity.class));
 352                    }
 353                    break;
 354                case R.id.create_private_group_chat:
 355                    showCreatePrivateGroupChatDialog();
 356                    break;
 357                case R.id.create_public_channel:
 358                    showPublicChannelDialog();
 359                    break;
 360                case R.id.create_contact:
 361                    showCreateContactDialog(prefilled, null);
 362                    break;
 363            }
 364            return false;
 365        });
 366    }
 367
 368    private void inflateFab(final SpeedDialView speedDialView, final @MenuRes int menuRes) {
 369        speedDialView.clearActionItems();
 370        final PopupMenu popupMenu = new PopupMenu(this, new View(this));
 371        popupMenu.inflate(menuRes);
 372        final Menu menu = popupMenu.getMenu();
 373        for (int i = 0; i < menu.size(); i++) {
 374            final MenuItem menuItem = menu.getItem(i);
 375            if (QuickConversationsService.isPlayStoreFlavor() && menuItem.getItemId() == R.id.discover_public_channels) {
 376                continue;
 377            }
 378            final SpeedDialActionItem actionItem = new SpeedDialActionItem.Builder(menuItem.getItemId(), menuItem.getIcon())
 379                    .setLabel(menuItem.getTitle() != null ? menuItem.getTitle().toString() : null)
 380                    .setFabImageTintColor(ContextCompat.getColor(this, R.color.white))
 381                    .create();
 382            speedDialView.addActionItem(actionItem);
 383        }
 384    }
 385
 386    public static boolean isValidJid(String input) {
 387        try {
 388            Jid jid = Jid.ofEscaped(input);
 389            return !jid.isDomainJid();
 390        } catch (IllegalArgumentException e) {
 391            return false;
 392        }
 393    }
 394
 395    @Override
 396    public void onSaveInstanceState(Bundle savedInstanceState) {
 397        Intent pendingIntent = pendingViewIntent.peek();
 398        savedInstanceState.putParcelable("intent", pendingIntent != null ? pendingIntent : getIntent());
 399        savedInstanceState.putBoolean("requested_contacts_permission", mRequestedContactsPermission.get());
 400        savedInstanceState.putBoolean("opened_fab", mOpenedFab.get());
 401        savedInstanceState.putBoolean("created_by_view_intent", createdByViewIntent);
 402        if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) {
 403            savedInstanceState.putString("search", mSearchEditText != null ? mSearchEditText.getText().toString() : null);
 404        }
 405        super.onSaveInstanceState(savedInstanceState);
 406    }
 407
 408    @Override
 409    public void onStart() {
 410        super.onStart();
 411        final int theme = findTheme();
 412        if (this.mTheme != theme) {
 413            recreate();
 414        } else {
 415            if (pendingViewIntent.peek() == null) {
 416                askForContactsPermissions();
 417            }
 418        }
 419        mConferenceAdapter.refreshSettings();
 420        mContactsAdapter.refreshSettings();
 421    }
 422
 423    @Override
 424    public void onNewIntent(final Intent intent) {
 425        super.onNewIntent(intent);
 426        if (xmppConnectionServiceBound) {
 427            processViewIntent(intent);
 428        } else {
 429            pendingViewIntent.push(intent);
 430        }
 431        setIntent(createLauncherIntent(this));
 432    }
 433
 434    protected void openConversationForContact(int position) {
 435        openConversation(contacts.get(position));
 436    }
 437
 438    protected void openConversation(ListItem item) {
 439        if (item instanceof Contact) {
 440            openConversationForContact((Contact) item);
 441        } else {
 442            openConversationsForBookmark((Bookmark) item);
 443        }
 444    }
 445
 446    protected void openConversationForContact(Contact contact) {
 447        Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
 448        SoftKeyboardUtils.hideSoftKeyboard(this);
 449        switchToConversation(conversation);
 450    }
 451
 452    protected void openConversationForBookmark(int position) {
 453        Bookmark bookmark = (Bookmark) conferences.get(position);
 454        openConversationsForBookmark(bookmark);
 455    }
 456
 457    protected void shareBookmarkUri() {
 458        shareAsChannel(this, contextItem.getJid().asBareJid().toEscapedString());
 459    }
 460
 461    protected void shareBookmarkUri(int position) {
 462        Bookmark bookmark = (Bookmark) conferences.get(position);
 463        shareAsChannel(this, bookmark.getJid().asBareJid().toEscapedString());
 464    }
 465
 466    public static void shareAsChannel(final Context context, final String address) {
 467        Intent shareIntent = new Intent();
 468        shareIntent.setAction(Intent.ACTION_SEND);
 469        shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:" + Uri.encode(address, "@/+") + "?join");
 470        shareIntent.setType("text/plain");
 471        try {
 472            context.startActivity(Intent.createChooser(shareIntent, context.getText(R.string.share_uri_with)));
 473        } catch (ActivityNotFoundException e) {
 474            Toast.makeText(context, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
 475        }
 476    }
 477
 478    protected void openConversationsForBookmark(Bookmark bookmark) {
 479        final Jid jid = bookmark.getFullJid();
 480        if (jid == null) {
 481            Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
 482            return;
 483        }
 484        Conversation conversation = xmppConnectionService.findOrCreateConversation(bookmark.getAccount(), jid, true, true, true);
 485        bookmark.setConversation(conversation);
 486        if (!bookmark.autojoin() && getPreferences().getBoolean("autojoin", getResources().getBoolean(R.bool.autojoin))) {
 487            bookmark.setAutojoin(true);
 488            xmppConnectionService.createBookmark(bookmark.getAccount(), bookmark);
 489        }
 490        SoftKeyboardUtils.hideSoftKeyboard(this);
 491        switchToConversation(conversation);
 492    }
 493
 494    protected void openDetailsForContact() {
 495        switchToContactDetails((Contact) contextItem);
 496    }
 497
 498    protected void showQrForContact() {
 499        showQrCode("xmpp:" + contextItem.getJid().asBareJid().toEscapedString());
 500    }
 501
 502    protected void toggleContactBlock() {
 503        BlockContactDialog.show(this, (Contact) contextItem);
 504    }
 505
 506    protected void deleteContact() {
 507        final Contact contact = (Contact) contextItem;
 508        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
 509        builder.setNegativeButton(R.string.cancel, null);
 510        builder.setTitle(R.string.action_delete_contact);
 511        builder.setMessage(JidDialog.style(this, R.string.remove_contact_text, contact.getJid().toEscapedString()));
 512        builder.setPositiveButton(R.string.delete, (dialog, which) -> {
 513            xmppConnectionService.deleteContactOnServer(contact);
 514            filter(mSearchEditText.getText().toString());
 515        });
 516        builder.create().show();
 517    }
 518
 519    protected void deleteConference() {
 520        final Bookmark bookmark = (Bookmark) contextItem;
 521
 522        AlertDialog.Builder builder = new AlertDialog.Builder(this);
 523        builder.setNegativeButton(R.string.cancel, null);
 524        builder.setTitle(R.string.delete_bookmark);
 525        builder.setMessage(JidDialog.style(this, R.string.remove_bookmark_text, bookmark.getJid().toEscapedString()));
 526        builder.setPositiveButton(R.string.delete, (dialog, which) -> {
 527            bookmark.setConversation(null);
 528            final Account account = bookmark.getAccount();
 529            xmppConnectionService.deleteBookmark(account, bookmark);
 530            filter(mSearchEditText.getText().toString());
 531        });
 532        builder.create().show();
 533
 534    }
 535
 536    @SuppressLint("InflateParams")
 537    protected void showCreateContactDialog(final String prefilledJid, final Invite invite) {
 538        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
 539        Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
 540        if (prev != null) {
 541            ft.remove(prev);
 542        }
 543        ft.addToBackStack(null);
 544        EnterJidDialog dialog = EnterJidDialog.newInstance(
 545                mActivatedAccounts,
 546                getString(R.string.start_conversation),
 547                getString(R.string.message),
 548                "Call",
 549                prefilledJid,
 550                invite == null ? null : invite.account,
 551                invite == null || !invite.hasFingerprints(),
 552                true,
 553                EnterJidDialog.SanityCheck.ALLOW_MUC
 554        );
 555
 556        dialog.setOnEnterJidDialogPositiveListener((accountJid, contactJid, call, save) -> {
 557            if (!xmppConnectionServiceBound) {
 558                return false;
 559            }
 560
 561            final Account account = xmppConnectionService.findAccountByJid(accountJid);
 562            if (account == null) {
 563                return true;
 564            }
 565            final Contact contact = account.getRoster().getContact(contactJid);
 566
 567            if (invite != null && invite.getName() != null) {
 568                contact.setServerName(invite.getName());
 569            }
 570
 571            if (contact.isSelf() || contact.showInRoster()) {
 572                switchToConversationDoNotAppend(contact, invite == null ? null : invite.getBody(), call ? "call" : null);
 573                return true;
 574            }
 575
 576            xmppConnectionService.checkIfMuc(account, contactJid, (isMuc) -> {
 577                runOnUiThread(() -> {
 578                    if (isMuc) {
 579                        if (save) {
 580                            Bookmark bookmark = account.getBookmark(contactJid);
 581                            if (bookmark != null) {
 582                                openConversationsForBookmark(bookmark);
 583                            } else {
 584                                bookmark = new Bookmark(account, contactJid.asBareJid());
 585                                bookmark.setAutojoin(getBooleanPreference("autojoin", R.bool.autojoin));
 586                                final String nick = contactJid.getResource();
 587                                if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
 588                                    bookmark.setNick(nick);
 589                                }
 590                                xmppConnectionService.createBookmark(account, bookmark);
 591                                final Conversation conversation = xmppConnectionService
 592                                        .findOrCreateConversation(account, contactJid, true, true, true);
 593                                bookmark.setConversation(conversation);
 594                                switchToConversationDoNotAppend(conversation, invite == null ? null : invite.getBody());
 595                            }
 596                        } else {
 597                            final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, contactJid, true, true, true);
 598                            switchToConversationDoNotAppend(conversation, invite == null ? null : invite.getBody());
 599                        }
 600                    } else {
 601                        if (save) {
 602                            final String preAuth = invite == null ? null : invite.getParameter(XmppUri.PARAMETER_PRE_AUTH);
 603                            xmppConnectionService.createContact(contact, true, preAuth);
 604                            if (invite != null && invite.hasFingerprints()) {
 605                                xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
 606                            }
 607                        }
 608                        switchToConversationDoNotAppend(contact, invite == null ? null : invite.getBody(), call ? "call" : null);
 609                    }
 610
 611                    try {
 612                        dialog.dismiss();
 613                    } catch (final IllegalStateException e) { }
 614                });
 615            });
 616
 617            return false;
 618        });
 619        dialog.show(ft, FRAGMENT_TAG_DIALOG);
 620    }
 621
 622    @SuppressLint("InflateParams")
 623    protected void showJoinConferenceDialog(final String prefilledJid) {
 624        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
 625        Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
 626        if (prev != null) {
 627            ft.remove(prev);
 628        }
 629        ft.addToBackStack(null);
 630        JoinConferenceDialog joinConferenceFragment = JoinConferenceDialog.newInstance(prefilledJid, mActivatedAccounts);
 631        joinConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG);
 632    }
 633
 634    private void showCreatePrivateGroupChatDialog() {
 635        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
 636        Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
 637        if (prev != null) {
 638            ft.remove(prev);
 639        }
 640        ft.addToBackStack(null);
 641        CreatePrivateGroupChatDialog createConferenceFragment = CreatePrivateGroupChatDialog.newInstance(mActivatedAccounts);
 642        createConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG);
 643    }
 644
 645    private void showPublicChannelDialog() {
 646        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
 647        Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
 648        if (prev != null) {
 649            ft.remove(prev);
 650        }
 651        ft.addToBackStack(null);
 652        CreatePublicChannelDialog dialog = CreatePublicChannelDialog.newInstance(mActivatedAccounts);
 653        dialog.show(ft, FRAGMENT_TAG_DIALOG);
 654    }
 655
 656    public static Account getSelectedAccount(Context context, Spinner spinner) {
 657        if (spinner == null || !spinner.isEnabled()) {
 658            return null;
 659        }
 660        if (context instanceof XmppActivity) {
 661            Jid jid;
 662            try {
 663                if (Config.DOMAIN_LOCK != null) {
 664                    jid = Jid.ofEscaped((String) spinner.getSelectedItem(), Config.DOMAIN_LOCK, null);
 665                } else {
 666                    jid = Jid.ofEscaped((String) spinner.getSelectedItem());
 667                }
 668            } catch (final IllegalArgumentException e) {
 669                return null;
 670            }
 671            final XmppConnectionService service = ((XmppActivity) context).xmppConnectionService;
 672            if (service == null) {
 673                return null;
 674            }
 675            return service.findAccountByJid(jid);
 676        } else {
 677            return null;
 678        }
 679    }
 680
 681    protected void switchToConversation(Contact contact) {
 682        Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
 683        switchToConversation(conversation);
 684    }
 685
 686    protected void switchToConversationDoNotAppend(Contact contact, String body) {
 687        switchToConversationDoNotAppend(contact, body, null);
 688    }
 689
 690    protected void switchToConversationDoNotAppend(Contact contact, String body, String postInit) {
 691        Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
 692        switchToConversation(conversation, body, false, null, false, true, postInit);
 693    }
 694
 695    @Override
 696    public void invalidateOptionsMenu() {
 697        boolean isExpanded = mMenuSearchView != null && mMenuSearchView.isActionViewExpanded();
 698        String text = mSearchEditText != null ? mSearchEditText.getText().toString() : "";
 699        if (isExpanded) {
 700            mInitialSearchValue.push(text);
 701            oneShotKeyboardSuppress.set(true);
 702        }
 703        super.invalidateOptionsMenu();
 704    }
 705
 706    private void updateSearchViewHint() {
 707        if (binding == null || mSearchEditText == null) {
 708            return;
 709        }
 710        if (binding.startConversationViewPager.getCurrentItem() == 0) {
 711            mSearchEditText.setHint(R.string.search_contacts);
 712            mSearchEditText.setContentDescription(getString(R.string.search_contacts));
 713        } else {
 714            mSearchEditText.setHint(R.string.search_group_chats);
 715            mSearchEditText.setContentDescription(getString(R.string.search_group_chats));
 716        }
 717    }
 718
 719    @Override
 720    public boolean onCreateOptionsMenu(Menu menu) {
 721        getMenuInflater().inflate(R.menu.start_conversation, menu);
 722        AccountUtils.showHideMenuItems(menu);
 723        MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline);
 724        MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
 725        qrCodeScanMenuItem.setVisible(isCameraFeatureAvailable());
 726        if (QuickConversationsService.isQuicksy()) {
 727            menuHideOffline.setVisible(false);
 728        } else {
 729            menuHideOffline.setVisible(true);
 730            menuHideOffline.setChecked(this.mHideOfflineContacts);
 731        }
 732        mMenuSearchView = menu.findItem(R.id.action_search);
 733        mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener);
 734        View mSearchView = mMenuSearchView.getActionView();
 735        mSearchEditText = mSearchView.findViewById(R.id.search_field);
 736        mSearchEditText.addTextChangedListener(mSearchTextWatcher);
 737        mSearchEditText.setOnEditorActionListener(mSearchDone);
 738
 739        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
 740        boolean showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, getResources().getBoolean(R.bool.show_dynamic_tags));
 741        if (showDynamicTags) {
 742            RecyclerView tags = mSearchView.findViewById(R.id.tags);
 743            tags.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
 744            tags.setAdapter(mTagsAdapter);
 745        }
 746
 747        String initialSearchValue = mInitialSearchValue.pop();
 748        if (initialSearchValue != null) {
 749            mMenuSearchView.expandActionView();
 750            try {
 751                mSearchEditText.append(initialSearchValue);
 752            } catch (final StringIndexOutOfBoundsException e) {
 753                mSearchEditText.setText(initialSearchValue);
 754            }
 755            filter(initialSearchValue);
 756        }
 757        updateSearchViewHint();
 758        return super.onCreateOptionsMenu(menu);
 759    }
 760
 761    @Override
 762    public boolean onOptionsItemSelected(MenuItem item) {
 763        if (MenuDoubleTabUtil.shouldIgnoreTap()) {
 764            return false;
 765        }
 766        switch (item.getItemId()) {
 767            case android.R.id.home:
 768                navigateBack();
 769                return true;
 770            case R.id.action_scan_qr_code:
 771                UriHandlerActivity.scan(this);
 772                return true;
 773            case R.id.action_hide_offline:
 774                mHideOfflineContacts = !item.isChecked();
 775                getPreferences().edit().putBoolean("hide_offline", mHideOfflineContacts).apply();
 776                if (mSearchEditText != null) {
 777                    filter(mSearchEditText.getText().toString());
 778                }
 779                invalidateOptionsMenu();
 780        }
 781        return super.onOptionsItemSelected(item);
 782    }
 783
 784    @Override
 785    public boolean onKeyUp(int keyCode, KeyEvent event) {
 786        if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) {
 787            openSearch();
 788            return true;
 789        }
 790        int c = event.getUnicodeChar();
 791        if (c > 32) {
 792            if (mSearchEditText != null && !mSearchEditText.isFocused()) {
 793                openSearch();
 794                mSearchEditText.append(Character.toString((char) c));
 795                return true;
 796            }
 797        }
 798        return super.onKeyUp(keyCode, event);
 799    }
 800
 801    private void openSearch() {
 802        if (mMenuSearchView != null) {
 803            mMenuSearchView.expandActionView();
 804        }
 805    }
 806
 807    @Override
 808    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
 809        if (resultCode == RESULT_OK) {
 810            if (xmppConnectionServiceBound) {
 811                this.mPostponedActivityResult = null;
 812                if (requestCode == REQUEST_CREATE_CONFERENCE) {
 813                    Account account = extractAccount(intent);
 814                    final String name = intent.getStringExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME);
 815                    final List<Jid> jids = ChooseContactActivity.extractJabberIds(intent);
 816                    if (account != null && jids.size() > 0) {
 817                        // This hardcodes cheogram.com and is in general a terrible hack
 818                        // Ideally this would be based around XEP-0033 but until we think of a good fallback behaviour we keep using this gross commas thing
 819                        if (jids.stream().allMatch(jid -> jid.getDomain().toString().equals("cheogram.com"))) {
 820                            new AlertDialog.Builder(this)
 821                                .setMessage("You appear to be creating a group with only SMS contacts. Would you like to create a channel or an MMS group text?")
 822                                .setNeutralButton("Channel", (d, w) -> {
 823                                    if (xmppConnectionService.createAdhocConference(account, name, jids, mAdhocConferenceCallback)) {
 824                                        mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
 825                                        mToast.show();
 826                                    }
 827                                }).setPositiveButton("Group Text", (d, w) -> {
 828                                    Jid groupJid = Jid.ofLocalAndDomain(jids.stream().map(jid -> jid.getLocal()).sorted().collect(Collectors.joining(",")), "cheogram.com");
 829                                    Contact group = account.getRoster().getContact(groupJid);
 830                                    if (name != null && !name.equals("")) group.setServerName(name);
 831                                    xmppConnectionService.createContact(group, true);
 832                                    switchToConversation(group);
 833                                }).create().show();
 834                        } else {
 835                            if (xmppConnectionService.createAdhocConference(account, name, jids, mAdhocConferenceCallback)) {
 836                                mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
 837                                mToast.show();
 838                            }
 839                        }
 840                    }
 841                }
 842            } else {
 843                this.mPostponedActivityResult = new Pair<>(requestCode, intent);
 844            }
 845        }
 846        super.onActivityResult(requestCode, requestCode, intent);
 847    }
 848
 849    private void askForContactsPermissions() {
 850        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
 851            if (checkSelfPermission(Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
 852                if (mRequestedContactsPermission.compareAndSet(false, true)) {
 853                    if (QuickConversationsService.isQuicksy() || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
 854                        final AlertDialog.Builder builder = new AlertDialog.Builder(this);
 855                        final AtomicBoolean requestPermission = new AtomicBoolean(false);
 856                        builder.setTitle(R.string.sync_with_contacts);
 857                        builder.setMessage(getString(R.string.sync_with_contacts_long, getString(R.string.app_name)));
 858                        builder.setPositiveButton(R.string.next, (dialog, which) -> {
 859                            if (requestPermission.compareAndSet(false, true)) {
 860                                requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
 861                            }
 862                        });
 863                        builder.setOnDismissListener(dialog -> {
 864                            if (QuickConversationsService.isConversations() && requestPermission.compareAndSet(false, true)) {
 865                                requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
 866                            }
 867                        });
 868                        if (QuickConversationsService.isQuicksy()) {
 869                            builder.setNegativeButton(R.string.decline, null);
 870                        }
 871                        builder.setCancelable(QuickConversationsService.isQuicksy());
 872                        final AlertDialog dialog = builder.create();
 873                        dialog.setCanceledOnTouchOutside(QuickConversationsService.isQuicksy());
 874                        dialog.setOnShowListener(dialogInterface -> {
 875                            final TextView tv = dialog.findViewById(android.R.id.message);
 876                            if (tv != null) {
 877                                tv.setMovementMethod(LinkMovementMethod.getInstance());
 878                            }
 879                        });
 880                        dialog.show();
 881                    } else {
 882                        requestPermissions(new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_SYNC_CONTACTS);
 883                    }
 884                }
 885            }
 886        }
 887    }
 888
 889    @Override
 890    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
 891        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
 892        if (grantResults.length > 0)
 893            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 894                ScanActivity.onRequestPermissionResult(this, requestCode, grantResults);
 895                if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
 896                    if (QuickConversationsService.isQuicksy()) {
 897                        setRefreshing(true);
 898                    }
 899                    xmppConnectionService.loadPhoneContacts();
 900                    xmppConnectionService.startContactObserver();
 901                }
 902            }
 903    }
 904
 905    private void configureHomeButton() {
 906        final ActionBar actionBar = getSupportActionBar();
 907        if (actionBar == null) {
 908            return;
 909        }
 910        boolean openConversations = !createdByViewIntent && !xmppConnectionService.isConversationsListEmpty(null);
 911        actionBar.setDisplayHomeAsUpEnabled(openConversations);
 912        actionBar.setDisplayHomeAsUpEnabled(openConversations);
 913
 914    }
 915
 916    @Override
 917    protected void onBackendConnected() {
 918
 919        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
 920            xmppConnectionService.getQuickConversationsService().considerSyncBackground(false);
 921        }
 922        if (mPostponedActivityResult != null) {
 923            onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
 924            this.mPostponedActivityResult = null;
 925        }
 926        this.mActivatedAccounts.clear();
 927        this.mActivatedAccounts.addAll(AccountUtils.getEnabledAccounts(xmppConnectionService));
 928        configureHomeButton();
 929        Intent intent = pendingViewIntent.pop();
 930
 931        final boolean onboardingCancel = xmppConnectionService.getPreferences().getString("onboarding_action", "").equals("cancel");
 932        if (onboardingCancel) xmppConnectionService.getPreferences().edit().remove("onboarding_action").commit();
 933
 934        if (intent != null && intent.getBooleanExtra("init", false) && !onboardingCancel && !xmppConnectionService.getAccounts().isEmpty()) {
 935            Account selectedAccount = xmppConnectionService.getAccounts().get(0);
 936            final String accountJid = intent.getStringExtra(EXTRA_ACCOUNT);
 937            intent = null;
 938            boolean hasPstnOrSms = false;
 939            Account onboardingAccount = null;
 940            outer:
 941            for (Account account : xmppConnectionService.getAccounts()) {
 942                if (onboardingAccount == null && account.getJid().getDomain().equals(Config.ONBOARDING_DOMAIN)) onboardingAccount = account;
 943
 944                if (accountJid != null) {
 945                    if(account.getJid().asBareJid().toEscapedString().equals(accountJid)) {
 946                        selectedAccount = account;
 947                    } else {
 948                        continue;
 949                    }
 950                }
 951
 952                for (Contact contact : account.getRoster().getContacts()) {
 953                    if (contact.getPresences().anyIdentity("gateway", "pstn")) {
 954                        hasPstnOrSms = true;
 955                        break outer;
 956                    }
 957                    if (contact.getPresences().anyIdentity("gateway", "sms")) {
 958                        hasPstnOrSms = true;
 959                        break outer;
 960                    }
 961                }
 962            }
 963
 964            if (!hasPstnOrSms) {
 965                if (onboardingAccount != null && !selectedAccount.getJid().equals(onboardingAccount.getJid())) {
 966                    FinishOnboarding.finish(xmppConnectionService, this, onboardingAccount, selectedAccount);
 967                } else {
 968                    startCommand(selectedAccount, Jid.of("cheogram.com/CHEOGRAM%jabber:iq:register"), "jabber:iq:register");
 969                    finish();
 970                    return;
 971                }
 972            }
 973        }
 974
 975        if (intent != null && processViewIntent(intent)) {
 976            filter(null);
 977        } else {
 978            if (mSearchEditText != null) {
 979                filter(mSearchEditText.getText().toString());
 980            } else {
 981                filter(null);
 982            }
 983        }
 984        Fragment fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
 985        if (fragment instanceof OnBackendConnected) {
 986            Log.d(Config.LOGTAG, "calling on backend connected on dialog");
 987            ((OnBackendConnected) fragment).onBackendConnected();
 988        }
 989        if (QuickConversationsService.isQuicksy()) {
 990            setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing());
 991        }
 992        if (QuickConversationsService.isConversations() && AccountUtils.hasEnabledAccounts(xmppConnectionService) && this.contacts.size() == 0 && this.conferences.size() == 0 && mOpenedFab.compareAndSet(false, true)) {
 993            binding.speedDial.open();
 994        }
 995    }
 996
 997    protected boolean processViewIntent(@NonNull Intent intent) {
 998        final String inviteUri = intent.getStringExtra(EXTRA_INVITE_URI);
 999        if (inviteUri != null) {
1000            final Invite invite = new Invite(inviteUri);
1001            invite.account = intent.getStringExtra(EXTRA_ACCOUNT);
1002            if (invite.isValidJid()) {
1003                return invite.invite();
1004            }
1005        }
1006        final String action = intent.getAction();
1007        if (action == null) {
1008            return false;
1009        }
1010        switch (action) {
1011            case Intent.ACTION_SENDTO:
1012            case Intent.ACTION_VIEW:
1013                Uri uri = intent.getData();
1014                if (uri != null) {
1015                    Invite invite = new Invite(intent.getData(), intent.getBooleanExtra("scanned", false));
1016                    invite.account = intent.getStringExtra(EXTRA_ACCOUNT);
1017                    invite.forceDialog = intent.getBooleanExtra("force_dialog", false);
1018                    return invite.invite();
1019                } else {
1020                    return false;
1021                }
1022        }
1023        return false;
1024    }
1025
1026    private boolean handleJid(Invite invite) {
1027        final List<Contact> contacts = xmppConnectionService.findContacts(invite.getJid(), invite.account);
1028        final Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid(), invite.account);
1029        if (invite.isAction(XmppUri.ACTION_JOIN) || (contacts.isEmpty() && muc != null)) {
1030            if (muc != null && !invite.forceDialog) {
1031                switchToConversationDoNotAppend(muc, invite.getBody());
1032                return true;
1033            } else {
1034                showJoinConferenceDialog(invite.getJid().asBareJid().toEscapedString());
1035                return false;
1036            }
1037        } else if (contacts.size() == 0) {
1038            showCreateContactDialog(invite.getJid().toEscapedString(), invite);
1039            return false;
1040        } else if (contacts.size() == 1) {
1041            Contact contact = contacts.get(0);
1042            if (!invite.isSafeSource() && invite.hasFingerprints()) {
1043                displayVerificationWarningDialog(contact, invite);
1044            } else {
1045                if (invite.hasFingerprints()) {
1046                    if (xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints())) {
1047                        Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
1048                    }
1049                }
1050                if (invite.account != null) {
1051                    xmppConnectionService.getShortcutService().report(contact);
1052                }
1053                switchToConversationDoNotAppend(contact, invite.getBody());
1054            }
1055            return true;
1056        } else {
1057            if (mMenuSearchView != null) {
1058                mMenuSearchView.expandActionView();
1059                mSearchEditText.setText("");
1060                mSearchEditText.append(invite.getJid().toEscapedString());
1061                filter(invite.getJid().toEscapedString());
1062            } else {
1063                mInitialSearchValue.push(invite.getJid().toEscapedString());
1064            }
1065            return true;
1066        }
1067    }
1068
1069    private void displayVerificationWarningDialog(final Contact contact, final Invite invite) {
1070        AlertDialog.Builder builder = new AlertDialog.Builder(this);
1071        builder.setTitle(R.string.verify_omemo_keys);
1072        View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
1073        final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source);
1074        TextView warning = view.findViewById(R.id.warning);
1075        warning.setText(JidDialog.style(this, R.string.verifying_omemo_keys_trusted_source, contact.getJid().asBareJid().toEscapedString(), contact.getDisplayName()));
1076        builder.setView(view);
1077        builder.setPositiveButton(R.string.confirm, (dialog, which) -> {
1078            if (isTrustedSource.isChecked() && invite.hasFingerprints()) {
1079                xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
1080            }
1081            switchToConversationDoNotAppend(contact, invite.getBody());
1082        });
1083        builder.setNegativeButton(R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish());
1084        AlertDialog dialog = builder.create();
1085        dialog.setCanceledOnTouchOutside(false);
1086        dialog.setOnCancelListener(dialog1 -> StartConversationActivity.this.finish());
1087        dialog.show();
1088    }
1089
1090    protected void filter(String needle) {
1091        if (xmppConnectionServiceBound) {
1092            this.filterContacts(needle);
1093            this.filterConferences(needle);
1094        }
1095    }
1096
1097    protected void filterContacts(String needle) {
1098        this.contacts.clear();
1099        ArrayList<ListItem.Tag> tags = new ArrayList<>();
1100        final List<Account> accounts = xmppConnectionService.getAccounts();
1101        boolean foundSopranica = false;
1102        for (final Account account : accounts) {
1103            if (account.isEnabled()) {
1104                for (Contact contact : account.getRoster().getContacts()) {
1105                    Presence.Status s = contact.getShownStatus();
1106                    if (contact.showInContactList() && contact.match(this, needle)
1107                            && (!this.mHideOfflineContacts
1108                            || (needle != null && !needle.trim().isEmpty())
1109                            || s.compareTo(Presence.Status.OFFLINE) < 0)) {
1110                        this.contacts.add(contact);
1111                        tags.addAll(contact.getTags(this));
1112                    }
1113                }
1114
1115                final Contact self = new Contact(account.getSelfContact());
1116                self.setSystemName("Note to Self");
1117                if (self.match(this, needle)) {
1118                    this.contacts.add(self);
1119                }
1120
1121                for (Bookmark bookmark : account.getBookmarks()) {
1122                    if (bookmark.match(this, needle)) {
1123                        if (bookmark.getJid().toString().equals("discuss@conference.soprani.ca")) {
1124                            foundSopranica = true;
1125                        }
1126                        this.contacts.add(bookmark);
1127                        tags.addAll(bookmark.getTags(this));
1128                    }
1129                }
1130            }
1131        }
1132
1133        Comparator<Map.Entry<ListItem.Tag,Integer>> sortTagsBy = Map.Entry.comparingByValue(Comparator.reverseOrder());
1134        sortTagsBy = sortTagsBy.thenComparing(entry -> entry.getKey().getName());
1135
1136        mTagsAdapter.setTags(
1137            tags.stream()
1138            .collect(Collectors.toMap((x) -> x, (t) -> 1, (c1, c2) -> c1 + c2))
1139            .entrySet().stream()
1140            .sorted(sortTagsBy)
1141            .map(e -> e.getKey()).collect(Collectors.toList())
1142        );
1143        Collections.sort(this.contacts);
1144
1145        final boolean sopranicaDeleted = getPreferences().getBoolean("cheogram_sopranica_bookmark_deleted", false);
1146
1147        if (!sopranicaDeleted && !foundSopranica && (needle == null || needle.equals("")) && xmppConnectionService.getAccounts().size() > 0) {
1148            Bookmark bookmark = new Bookmark(
1149                xmppConnectionService.getAccounts().get(0),
1150                Jid.of("discuss@conference.soprani.ca")
1151            );
1152            bookmark.setBookmarkName("Soprani.ca / Cheogram Discussion");
1153            bookmark.addChild("group").setContent("support");
1154            this.contacts.add(0, bookmark);
1155        }
1156
1157        mContactsAdapter.notifyDataSetChanged();
1158    }
1159
1160    protected void filterConferences(String needle) {
1161        this.conferences.clear();
1162        for (final Account account : xmppConnectionService.getAccounts()) {
1163            if (account.isEnabled()) {
1164                for (final Bookmark bookmark : account.getBookmarks()) {
1165                    if (bookmark.match(this, needle)) {
1166                        this.conferences.add(bookmark);
1167                    }
1168                }
1169            }
1170        }
1171        Collections.sort(this.conferences);
1172        mConferenceAdapter.notifyDataSetChanged();
1173    }
1174
1175    @Override
1176    public void OnUpdateBlocklist(final Status status) {
1177        refreshUi();
1178    }
1179
1180    @Override
1181    protected void refreshUiReal() {
1182        if (mSearchEditText != null) {
1183            filter(mSearchEditText.getText().toString());
1184        }
1185        configureHomeButton();
1186        if (QuickConversationsService.isQuicksy()) {
1187            setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing());
1188        }
1189    }
1190
1191    @Override
1192    public void onBackPressed() {
1193        if (binding.speedDial.isOpen()) {
1194            binding.speedDial.close();
1195            return;
1196        }
1197        navigateBack();
1198    }
1199
1200    private void navigateBack() {
1201        if (!createdByViewIntent && xmppConnectionService != null && !xmppConnectionService.isConversationsListEmpty(null)) {
1202            Intent intent = new Intent(this, ConversationsActivity.class);
1203            intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
1204            startActivity(intent);
1205        }
1206        finish();
1207    }
1208
1209    @Override
1210    public void onCreateDialogPositiveClick(Spinner spinner, String name) {
1211        if (!xmppConnectionServiceBound) {
1212            return;
1213        }
1214        final Account account = getSelectedAccount(this, spinner);
1215        if (account == null) {
1216            return;
1217        }
1218        Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class);
1219        intent.putExtra(ChooseContactActivity.EXTRA_SHOW_ENTER_JID, false);
1220        intent.putExtra(ChooseContactActivity.EXTRA_SELECT_MULTIPLE, true);
1221        intent.putExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME, name.trim());
1222        intent.putExtra(ChooseContactActivity.EXTRA_ACCOUNT, account.getJid().asBareJid().toEscapedString());
1223        intent.putExtra(ChooseContactActivity.EXTRA_TITLE_RES_ID, R.string.choose_participants);
1224        startActivityForResult(intent, REQUEST_CREATE_CONFERENCE);
1225    }
1226
1227    @Override
1228    public void onJoinDialogPositiveClick(Dialog dialog, Spinner spinner, TextInputLayout layout, AutoCompleteTextView jid, boolean isBookmarkChecked) {
1229        if (!xmppConnectionServiceBound) {
1230            return;
1231        }
1232        final Account account = getSelectedAccount(this, spinner);
1233        if (account == null) {
1234            return;
1235        }
1236        final String input = jid.getText().toString().trim();
1237        Jid conferenceJid;
1238        try {
1239            conferenceJid = Jid.ofEscaped(input);
1240        } catch (final IllegalArgumentException e) {
1241            final XmppUri xmppUri = new XmppUri(input);
1242            if (xmppUri.isValidJid() && xmppUri.isAction(XmppUri.ACTION_JOIN)) {
1243                final Editable editable = jid.getEditableText();
1244                editable.clear();
1245                editable.append(xmppUri.getJid().toEscapedString());
1246                conferenceJid = xmppUri.getJid();
1247            } else {
1248                layout.setError(getString(R.string.invalid_jid));
1249                return;
1250            }
1251        }
1252
1253        if (isBookmarkChecked) {
1254            Bookmark bookmark = account.getBookmark(conferenceJid);
1255            if (bookmark != null) {
1256                dialog.dismiss();
1257                openConversationsForBookmark(bookmark);
1258            } else {
1259                bookmark = new Bookmark(account, conferenceJid.asBareJid());
1260                bookmark.setAutojoin(getBooleanPreference("autojoin", R.bool.autojoin));
1261                final String nick = conferenceJid.getResource();
1262                if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
1263                    bookmark.setNick(nick);
1264                }
1265                xmppConnectionService.createBookmark(account, bookmark);
1266                final Conversation conversation = xmppConnectionService
1267                        .findOrCreateConversation(account, conferenceJid, true, true, true);
1268                bookmark.setConversation(conversation);
1269                dialog.dismiss();
1270                switchToConversation(conversation);
1271            }
1272        } else {
1273            final Conversation conversation = xmppConnectionService
1274                    .findOrCreateConversation(account, conferenceJid, true, true, true);
1275            dialog.dismiss();
1276            switchToConversation(conversation);
1277        }
1278    }
1279
1280    @Override
1281    public void onConversationUpdate() {
1282        refreshUi();
1283    }
1284
1285    @Override
1286    public void onRefresh() {
1287        Log.d(Config.LOGTAG, "user requested to refresh");
1288        if (QuickConversationsService.isQuicksy() && xmppConnectionService != null) {
1289            xmppConnectionService.getQuickConversationsService().considerSyncBackground(true);
1290        }
1291    }
1292
1293
1294    private void setRefreshing(boolean refreshing) {
1295        MyListFragment fragment = (MyListFragment) mListPagerAdapter.getItem(0);
1296        if (fragment != null) {
1297            fragment.setRefreshing(refreshing);
1298        }
1299    }
1300
1301    @Override
1302    public void onCreatePublicChannel(Account account, String name, Jid address) {
1303        mToast = Toast.makeText(this, R.string.creating_channel, Toast.LENGTH_LONG);
1304        mToast.show();
1305        xmppConnectionService.createPublicChannel(account, name, address, new UiCallback<Conversation>() {
1306            @Override
1307            public void success(Conversation conversation) {
1308                runOnUiThread(() -> {
1309                    hideToast();
1310                    switchToConversation(conversation);
1311                });
1312
1313            }
1314
1315            @Override
1316            public void error(int errorCode, Conversation conversation) {
1317                runOnUiThread(() -> {
1318                    replaceToast(getString(errorCode));
1319                    switchToConversation(conversation);
1320                });
1321            }
1322
1323            @Override
1324            public void userInputRequired(PendingIntent pi, Conversation object) {
1325
1326            }
1327        });
1328    }
1329
1330    public static class MyListFragment extends SwipeRefreshListFragment {
1331        private AdapterView.OnItemClickListener mOnItemClickListener;
1332        private int mResContextMenu;
1333
1334        public void setContextMenu(final int res) {
1335            this.mResContextMenu = res;
1336        }
1337
1338        @Override
1339        public void onListItemClick(final ListView l, final View v, final int position, final long id) {
1340            if (mOnItemClickListener != null) {
1341                mOnItemClickListener.onItemClick(l, v, position, id);
1342            }
1343        }
1344
1345        public void setOnListItemClickListener(AdapterView.OnItemClickListener l) {
1346            this.mOnItemClickListener = l;
1347        }
1348
1349        @Override
1350        public void onViewCreated(@NonNull final View view, final Bundle savedInstanceState) {
1351            super.onViewCreated(view, savedInstanceState);
1352            registerForContextMenu(getListView());
1353            getListView().setFastScrollEnabled(true);
1354            getListView().setDivider(null);
1355            getListView().setDividerHeight(0);
1356        }
1357
1358        @Override
1359        public void onCreateContextMenu(final ContextMenu menu, final View v, final ContextMenuInfo menuInfo) {
1360            super.onCreateContextMenu(menu, v, menuInfo);
1361            final StartConversationActivity activity = (StartConversationActivity) getActivity();
1362            if (activity == null) {
1363                return;
1364            }
1365            final AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
1366            activity.contextItem = null;
1367            if (mResContextMenu == R.menu.contact_context) {
1368                activity.contextItem = activity.contacts.get(acmi.position);
1369            } else if (mResContextMenu == R.menu.conference_context) {
1370                activity.contextItem = activity.conferences.get(acmi.position);
1371            }
1372            if (activity.contextItem instanceof Bookmark) {
1373                activity.getMenuInflater().inflate(R.menu.conference_context, menu);
1374                final Bookmark bookmark = (Bookmark) activity.contextItem;
1375                final Conversation conversation = bookmark.getConversation();
1376                final MenuItem share = menu.findItem(R.id.context_share_uri);
1377                share.setVisible(conversation == null || !conversation.isPrivateAndNonAnonymous());
1378            } else if (activity.contextItem instanceof Contact) {
1379                activity.getMenuInflater().inflate(R.menu.contact_context, menu);
1380                final Contact contact = (Contact) activity.contextItem;
1381                final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock);
1382                final MenuItem showContactDetailsItem = menu.findItem(R.id.context_contact_details);
1383                final MenuItem deleteContactMenuItem = menu.findItem(R.id.context_delete_contact);
1384                if (contact.isSelf()) {
1385                    showContactDetailsItem.setVisible(false);
1386                }
1387                deleteContactMenuItem.setVisible(contact.showInRoster() && !contact.getOption(Contact.Options.SYNCED_VIA_OTHER));
1388                final XmppConnection xmpp = contact.getAccount().getXmppConnection();
1389                if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) {
1390                    if (contact.isBlocked()) {
1391                        blockUnblockItem.setTitle(R.string.unblock_contact);
1392                    } else {
1393                        blockUnblockItem.setTitle(R.string.block_contact);
1394                    }
1395                } else {
1396                    blockUnblockItem.setVisible(false);
1397                }
1398            }
1399        }
1400
1401        @Override
1402        public boolean onContextItemSelected(final MenuItem item) {
1403            StartConversationActivity activity = (StartConversationActivity) getActivity();
1404            if (activity == null) {
1405                return true;
1406            }
1407            switch (item.getItemId()) {
1408                case R.id.context_contact_details:
1409                    activity.openDetailsForContact();
1410                    break;
1411                case R.id.context_show_qr:
1412                    activity.showQrForContact();
1413                    break;
1414                case R.id.context_contact_block_unblock:
1415                    activity.toggleContactBlock();
1416                    break;
1417                case R.id.context_delete_contact:
1418                    activity.deleteContact();
1419                    break;
1420                case R.id.context_share_uri:
1421                    activity.shareBookmarkUri();
1422                    break;
1423                case R.id.context_delete_conference:
1424                    activity.deleteConference();
1425            }
1426            return true;
1427        }
1428    }
1429
1430    public class ListPagerAdapter extends PagerAdapter {
1431        private final FragmentManager fragmentManager;
1432        private final MyListFragment[] fragments;
1433
1434        ListPagerAdapter(FragmentManager fm) {
1435            fragmentManager = fm;
1436            fragments = new MyListFragment[2];
1437        }
1438
1439        public void requestFocus(int pos) {
1440            if (fragments.length > pos) {
1441                fragments[pos].getListView().requestFocus();
1442            }
1443        }
1444
1445        @Override
1446        public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
1447            FragmentTransaction trans = fragmentManager.beginTransaction();
1448            trans.remove(fragments[position]);
1449            trans.commit();
1450            fragments[position] = null;
1451        }
1452
1453        @NonNull
1454        @Override
1455        public Fragment instantiateItem(@NonNull ViewGroup container, int position) {
1456            final Fragment fragment = getItem(position);
1457            final FragmentTransaction trans = fragmentManager.beginTransaction();
1458            trans.add(container.getId(), fragment, "fragment:" + position);
1459            try {
1460                trans.commit();
1461            } catch (IllegalStateException e) {
1462                //ignore
1463            }
1464            return fragment;
1465        }
1466
1467        @Override
1468        public int getCount() {
1469            return fragments.length;
1470        }
1471
1472        @Override
1473        public boolean isViewFromObject(@NonNull View view, @NonNull Object fragment) {
1474            return ((Fragment) fragment).getView() == view;
1475        }
1476
1477        @Nullable
1478        @Override
1479        public CharSequence getPageTitle(int position) {
1480            switch (position) {
1481                case 0:
1482                    return getResources().getString(R.string.contacts);
1483                case 1:
1484                    return getResources().getString(R.string.group_chats);
1485                default:
1486                    return super.getPageTitle(position);
1487            }
1488        }
1489
1490        Fragment getItem(int position) {
1491            if (fragments[position] == null) {
1492                final MyListFragment listFragment = new MyListFragment();
1493                if (position == 1) {
1494                    listFragment.setListAdapter(mConferenceAdapter);
1495                    listFragment.setContextMenu(R.menu.conference_context);
1496                    listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForBookmark(p));
1497                } else {
1498                    listFragment.setListAdapter(mContactsAdapter);
1499                    listFragment.setContextMenu(R.menu.contact_context);
1500                    listFragment.setOnListItemClickListener((arg0, arg1, p, arg3) -> openConversationForContact(p));
1501                    if (QuickConversationsService.isQuicksy()) {
1502                        listFragment.setOnRefreshListener(StartConversationActivity.this);
1503                    }
1504                }
1505                fragments[position] = listFragment;
1506            }
1507            return fragments[position];
1508        }
1509    }
1510
1511    public static void addInviteUri(Intent to, Intent from) {
1512        if (from != null && from.hasExtra(EXTRA_INVITE_URI)) {
1513            final String invite = from.getStringExtra(EXTRA_INVITE_URI);
1514            to.putExtra(EXTRA_INVITE_URI, invite);
1515        }
1516    }
1517
1518    private class Invite extends XmppUri {
1519
1520        public String account;
1521
1522        boolean forceDialog = false;
1523
1524
1525        Invite(final String uri) {
1526            super(uri);
1527        }
1528
1529        Invite(Uri uri, boolean safeSource) {
1530            super(uri, safeSource);
1531        }
1532
1533        boolean invite() {
1534            if (!isValidJid()) {
1535                Toast.makeText(StartConversationActivity.this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
1536                return false;
1537            }
1538            if (getJid() != null) {
1539                return handleJid(this);
1540            }
1541            return false;
1542        }
1543    }
1544
1545    class TagsAdapter extends RecyclerView.Adapter<TagsAdapter.ViewHolder> {
1546        class ViewHolder extends RecyclerView.ViewHolder {
1547            protected TextView tv;
1548
1549            public ViewHolder(View v) {
1550                super(v);
1551                tv = (TextView) v;
1552                tv.setOnClickListener(view -> {
1553                    String needle = mSearchEditText.getText().toString();
1554                    String tag = tv.getText().toString();
1555                    String[] parts = needle.split("[,\\s]+");
1556                    if(needle.isEmpty()) {
1557                        needle = tag;
1558                    } else if (tag.toLowerCase(Locale.US).contains(parts[parts.length-1])) {
1559                        needle = needle.replace(parts[parts.length-1], tag);
1560                    } else {
1561                        needle += ", " + tag;
1562                    }
1563                    mSearchEditText.setText("");
1564                    mSearchEditText.append(needle);
1565                    filter(needle);
1566                });
1567            }
1568
1569            public void setTag(ListItem.Tag tag) {
1570                tv.setText(tag.getName());
1571                tv.setBackgroundColor(tag.getColor());
1572            }
1573        }
1574
1575        protected List<ListItem.Tag> tags = new ArrayList<>();
1576
1577        @Override
1578        public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
1579            View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.list_item_tag, null);
1580            return new ViewHolder(view);
1581        }
1582
1583        @Override
1584        public void onBindViewHolder(ViewHolder viewHolder, int i) {
1585            viewHolder.setTag(tags.get(i));
1586        }
1587
1588        @Override
1589        public int getItemCount() {
1590            return tags.size();
1591        }
1592
1593        public void setTags(final List<ListItem.Tag> tags) {
1594            ListItem.Tag channelTag = new ListItem.Tag("Channel", UIHelper.getColorForName("Channel", true));
1595            String needle = mSearchEditText == null ? "" : mSearchEditText.getText().toString().toLowerCase(Locale.US).trim();
1596            HashSet<String> parts = new HashSet<>(Arrays.asList(needle.split("[,\\s]+")));
1597            this.tags = tags.stream().filter(
1598                tag -> !tag.equals(channelTag) && !parts.contains(tag.getName().toLowerCase(Locale.US))
1599            ).collect(Collectors.toList());
1600            if (!parts.contains("channel") && tags.contains(channelTag)) this.tags.add(0, channelTag);
1601            notifyDataSetChanged();
1602        }
1603    }
1604}