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