StartConversationActivity.java

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