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