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