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        speedDialView.setContentDescription(getString(R.string.add_contact_or_create_or_join_group_chat));
 422    }
 423
 424    public static boolean isValidJid(String input) {
 425        try {
 426            Jid jid = Jid.ofEscaped(input);
 427            return !jid.isDomainJid();
 428        } catch (IllegalArgumentException e) {
 429            return false;
 430        }
 431    }
 432
 433    @Override
 434    public void onSaveInstanceState(Bundle savedInstanceState) {
 435        Intent pendingIntent = pendingViewIntent.peek();
 436        savedInstanceState.putParcelable(
 437                "intent", pendingIntent != null ? pendingIntent : getIntent());
 438        savedInstanceState.putBoolean(
 439                "requested_contacts_permission", mRequestedContactsPermission.get());
 440        savedInstanceState.putBoolean("opened_fab", mOpenedFab.get());
 441        savedInstanceState.putBoolean("created_by_view_intent", createdByViewIntent);
 442        if (mMenuSearchView != null && mMenuSearchView.isActionViewExpanded()) {
 443            savedInstanceState.putString(
 444                    "search",
 445                    mSearchEditText != null ? mSearchEditText.getText().toString() : null);
 446        }
 447        super.onSaveInstanceState(savedInstanceState);
 448    }
 449
 450    @Override
 451    public void onStart() {
 452        super.onStart();
 453        mConferenceAdapter.refreshSettings();
 454        mContactsAdapter.refreshSettings();
 455        if (pendingViewIntent.peek() == null) {
 456            if (askForContactsPermissions()) {
 457                return;
 458            }
 459            requestNotificationPermissionIfNeeded();
 460        }
 461    }
 462
 463    private void requestNotificationPermissionIfNeeded() {
 464        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
 465                && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
 466                        != PackageManager.PERMISSION_GRANTED) {
 467            requestPermissions(
 468                    new String[] {Manifest.permission.POST_NOTIFICATIONS},
 469                    REQUEST_POST_NOTIFICATION);
 470        }
 471    }
 472
 473    @Override
 474    public void onNewIntent(final Intent intent) {
 475        super.onNewIntent(intent);
 476        if (xmppConnectionServiceBound) {
 477            processViewIntent(intent);
 478        } else {
 479            pendingViewIntent.push(intent);
 480        }
 481        setIntent(createLauncherIntent(this));
 482    }
 483
 484    protected void openConversationForContact(int position) {
 485        Contact contact = (Contact) contacts.get(position);
 486        openConversationForContact(contact);
 487    }
 488
 489    protected void openConversationForContact(Contact contact) {
 490        Conversation conversation =
 491                xmppConnectionService.findOrCreateConversation(
 492                        contact.getAccount(), contact.getJid(), false, true);
 493        SoftKeyboardUtils.hideSoftKeyboard(this);
 494        switchToConversation(conversation);
 495    }
 496
 497    protected void openConversationForBookmark(int position) {
 498        Bookmark bookmark = (Bookmark) conferences.get(position);
 499        openConversationsForBookmark(bookmark);
 500    }
 501
 502    protected void shareBookmarkUri() {
 503        shareBookmarkUri(conference_context_id);
 504    }
 505
 506    protected void shareBookmarkUri(int position) {
 507        Bookmark bookmark = (Bookmark) conferences.get(position);
 508        shareAsChannel(this, bookmark.getJid().asBareJid().toEscapedString());
 509    }
 510
 511    public static void shareAsChannel(final Context context, final String address) {
 512        Intent shareIntent = new Intent();
 513        shareIntent.setAction(Intent.ACTION_SEND);
 514        shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:" + address + "?join");
 515        shareIntent.setType("text/plain");
 516        try {
 517            context.startActivity(
 518                    Intent.createChooser(shareIntent, context.getText(R.string.share_uri_with)));
 519        } catch (ActivityNotFoundException e) {
 520            Toast.makeText(context, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT)
 521                    .show();
 522        }
 523    }
 524
 525    protected void openConversationsForBookmark(final Bookmark bookmark) {
 526        final Jid jid = bookmark.getFullJid();
 527        if (jid == null) {
 528            Toast.makeText(this, R.string.invalid_jid, Toast.LENGTH_SHORT).show();
 529            return;
 530        }
 531        final Conversation conversation =
 532                xmppConnectionService.findOrCreateConversation(
 533                        bookmark.getAccount(), jid, true, true, true);
 534        bookmark.setConversation(conversation);
 535        if (!bookmark.autojoin()) {
 536            bookmark.setAutojoin(true);
 537            xmppConnectionService.createBookmark(bookmark.getAccount(), bookmark);
 538        }
 539        SoftKeyboardUtils.hideSoftKeyboard(this);
 540        switchToConversation(conversation);
 541    }
 542
 543    protected void openDetailsForContact() {
 544        int position = contact_context_id;
 545        Contact contact = (Contact) contacts.get(position);
 546        switchToContactDetails(contact);
 547    }
 548
 549    protected void showQrForContact() {
 550        int position = contact_context_id;
 551        Contact contact = (Contact) contacts.get(position);
 552        showQrCode("xmpp:" + contact.getJid().asBareJid().toEscapedString());
 553    }
 554
 555    protected void toggleContactBlock() {
 556        final int position = contact_context_id;
 557        BlockContactDialog.show(this, (Contact) contacts.get(position));
 558    }
 559
 560    protected void deleteContact() {
 561        final int position = contact_context_id;
 562        final Contact contact = (Contact) contacts.get(position);
 563        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
 564        builder.setNegativeButton(R.string.cancel, null);
 565        builder.setTitle(R.string.action_delete_contact);
 566        builder.setMessage(
 567                JidDialog.style(
 568                        this, R.string.remove_contact_text, contact.getJid().toEscapedString()));
 569        builder.setPositiveButton(
 570                R.string.delete,
 571                (dialog, which) -> {
 572                    xmppConnectionService.deleteContactOnServer(contact);
 573                    filter(mSearchEditText.getText().toString());
 574                });
 575        builder.create().show();
 576    }
 577
 578    protected void deleteConference() {
 579        final int position = conference_context_id;
 580        final Bookmark bookmark = (Bookmark) conferences.get(position);
 581        final var conversation = bookmark.getConversation();
 582        final boolean hasConversation = conversation != null;
 583        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
 584        builder.setNegativeButton(R.string.cancel, null);
 585        builder.setTitle(R.string.delete_bookmark);
 586        if (hasConversation) {
 587            builder.setMessage(
 588                    JidDialog.style(
 589                            this,
 590                            R.string.remove_bookmark_and_close,
 591                            bookmark.getJid().toEscapedString()));
 592        } else {
 593            builder.setMessage(
 594                    JidDialog.style(
 595                            this, R.string.remove_bookmark, bookmark.getJid().toEscapedString()));
 596        }
 597        builder.setPositiveButton(
 598                hasConversation ? R.string.delete_and_close : R.string.delete,
 599                (dialog, which) -> {
 600                    bookmark.setConversation(null);
 601                    final Account account = bookmark.getAccount();
 602                    xmppConnectionService.deleteBookmark(account, bookmark);
 603                    if (conversation != null) {
 604                        xmppConnectionService.archiveConversation(conversation);
 605                    }
 606                    filter(mSearchEditText.getText().toString());
 607                });
 608        builder.create().show();
 609    }
 610
 611    @SuppressLint("InflateParams")
 612    protected void showCreateContactDialog(final String prefilledJid, final Invite invite) {
 613        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
 614        Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
 615        if (prev != null) {
 616            ft.remove(prev);
 617        }
 618        ft.addToBackStack(null);
 619        EnterJidDialog dialog =
 620                EnterJidDialog.newInstance(
 621                        mActivatedAccounts,
 622                        getString(R.string.add_contact),
 623                        getString(R.string.add),
 624                        prefilledJid,
 625                        invite == null ? null : invite.account,
 626                        invite == null || !invite.hasFingerprints(),
 627                        true);
 628
 629        dialog.setOnEnterJidDialogPositiveListener(
 630                (accountJid, contactJid) -> {
 631                    if (!xmppConnectionServiceBound) {
 632                        return false;
 633                    }
 634
 635                    final Account account = xmppConnectionService.findAccountByJid(accountJid);
 636                    if (account == null) {
 637                        return true;
 638                    }
 639
 640                    final Contact contact = account.getRoster().getContact(contactJid);
 641                    if (invite != null && invite.getName() != null) {
 642                        contact.setServerName(invite.getName());
 643                    }
 644                    if (contact.isSelf()) {
 645                        switchToConversation(contact);
 646                        return true;
 647                    } else if (contact.showInRoster()) {
 648                        throw new EnterJidDialog.JidError(
 649                                getString(R.string.contact_already_exists));
 650                    } else {
 651                        final String preAuth =
 652                                invite == null
 653                                        ? null
 654                                        : invite.getParameter(XmppUri.PARAMETER_PRE_AUTH);
 655                        xmppConnectionService.createContact(contact, true, preAuth);
 656                        if (invite != null && invite.hasFingerprints()) {
 657                            xmppConnectionService.verifyFingerprints(
 658                                    contact, invite.getFingerprints());
 659                        }
 660                        switchToConversationDoNotAppend(
 661                                contact, invite == null ? null : invite.getBody());
 662                        return true;
 663                    }
 664                });
 665        dialog.show(ft, FRAGMENT_TAG_DIALOG);
 666    }
 667
 668    @SuppressLint("InflateParams")
 669    protected void showJoinConferenceDialog(final String prefilledJid) {
 670        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
 671        Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
 672        if (prev != null) {
 673            ft.remove(prev);
 674        }
 675        ft.addToBackStack(null);
 676        JoinConferenceDialog joinConferenceFragment =
 677                JoinConferenceDialog.newInstance(prefilledJid, mActivatedAccounts);
 678        joinConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG);
 679    }
 680
 681    private void showCreatePrivateGroupChatDialog() {
 682        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
 683        Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
 684        if (prev != null) {
 685            ft.remove(prev);
 686        }
 687        ft.addToBackStack(null);
 688        CreatePrivateGroupChatDialog createConferenceFragment =
 689                CreatePrivateGroupChatDialog.newInstance(mActivatedAccounts);
 690        createConferenceFragment.show(ft, FRAGMENT_TAG_DIALOG);
 691    }
 692
 693    private void showPublicChannelDialog() {
 694        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
 695        Fragment prev = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
 696        if (prev != null) {
 697            ft.remove(prev);
 698        }
 699        ft.addToBackStack(null);
 700        CreatePublicChannelDialog dialog =
 701                CreatePublicChannelDialog.newInstance(mActivatedAccounts);
 702        dialog.show(ft, FRAGMENT_TAG_DIALOG);
 703    }
 704
 705    public static Account getSelectedAccount(
 706            final Context context, final AutoCompleteTextView spinner) {
 707        if (spinner == null || !spinner.isEnabled()) {
 708            return null;
 709        }
 710        if (context instanceof XmppActivity) {
 711            final Jid jid;
 712            try {
 713                jid = Jid.ofEscaped(spinner.getText().toString());
 714            } catch (final IllegalArgumentException e) {
 715                return null;
 716            }
 717            final XmppConnectionService service = ((XmppActivity) context).xmppConnectionService;
 718            if (service == null) {
 719                return null;
 720            }
 721            return service.findAccountByJid(jid);
 722        } else {
 723            return null;
 724        }
 725    }
 726
 727    protected void switchToConversation(Contact contact) {
 728        Conversation conversation =
 729                xmppConnectionService.findOrCreateConversation(
 730                        contact.getAccount(), contact.getJid(), false, true);
 731        switchToConversation(conversation);
 732    }
 733
 734    protected void switchToConversationDoNotAppend(Contact contact, String body) {
 735        Conversation conversation =
 736                xmppConnectionService.findOrCreateConversation(
 737                        contact.getAccount(), contact.getJid(), false, true);
 738        switchToConversationDoNotAppend(conversation, body);
 739    }
 740
 741    @Override
 742    public void invalidateOptionsMenu() {
 743        boolean isExpanded = mMenuSearchView != null && mMenuSearchView.isActionViewExpanded();
 744        String text = mSearchEditText != null ? mSearchEditText.getText().toString() : "";
 745        if (isExpanded) {
 746            mInitialSearchValue.push(text);
 747            oneShotKeyboardSuppress.set(true);
 748        }
 749        super.invalidateOptionsMenu();
 750    }
 751
 752    private void updateSearchViewHint() {
 753        if (binding == null || mSearchEditText == null) {
 754            return;
 755        }
 756        if (binding.startConversationViewPager.getCurrentItem() == 0) {
 757            mSearchEditText.setHint(R.string.search_contacts);
 758            mSearchEditText.setContentDescription(getString(R.string.search_contacts));
 759        } else {
 760            mSearchEditText.setHint(R.string.search_group_chats);
 761            mSearchEditText.setContentDescription(getString(R.string.search_group_chats));
 762        }
 763    }
 764
 765    @Override
 766    public boolean onCreateOptionsMenu(final Menu menu) {
 767        getMenuInflater().inflate(R.menu.start_conversation, menu);
 768        AccountUtils.showHideMenuItems(menu);
 769        final MenuItem menuHideOffline = menu.findItem(R.id.action_hide_offline);
 770        final MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
 771        final MenuItem privacyPolicyMenuItem = menu.findItem(R.id.action_privacy_policy);
 772        privacyPolicyMenuItem.setVisible(
 773                BuildConfig.PRIVACY_POLICY != null
 774                        && QuickConversationsService.isPlayStoreFlavor());
 775        qrCodeScanMenuItem.setVisible(isCameraFeatureAvailable());
 776        if (QuickConversationsService.isQuicksy()) {
 777            menuHideOffline.setVisible(false);
 778        } else {
 779            menuHideOffline.setVisible(true);
 780            menuHideOffline.setChecked(this.mHideOfflineContacts);
 781        }
 782        mMenuSearchView = menu.findItem(R.id.action_search);
 783        mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener);
 784        View mSearchView = mMenuSearchView.getActionView();
 785        mSearchEditText = mSearchView.findViewById(R.id.search_field);
 786        mSearchEditText.addTextChangedListener(mSearchTextWatcher);
 787        mSearchEditText.setOnEditorActionListener(mSearchDone);
 788        String initialSearchValue = mInitialSearchValue.pop();
 789        if (initialSearchValue != null) {
 790            mMenuSearchView.expandActionView();
 791            mSearchEditText.append(initialSearchValue);
 792            filter(initialSearchValue);
 793        }
 794        updateSearchViewHint();
 795        return super.onCreateOptionsMenu(menu);
 796    }
 797
 798    @Override
 799    public boolean onOptionsItemSelected(MenuItem item) {
 800        if (MenuDoubleTabUtil.shouldIgnoreTap()) {
 801            return false;
 802        }
 803        switch (item.getItemId()) {
 804            case android.R.id.home:
 805                navigateBack();
 806                return true;
 807            case R.id.action_scan_qr_code:
 808                UriHandlerActivity.scan(this);
 809                return true;
 810            case R.id.action_hide_offline:
 811                mHideOfflineContacts = !item.isChecked();
 812                getPreferences().edit().putBoolean("hide_offline", mHideOfflineContacts).apply();
 813                if (mSearchEditText != null) {
 814                    filter(mSearchEditText.getText().toString());
 815                }
 816                invalidateOptionsMenu();
 817        }
 818        return super.onOptionsItemSelected(item);
 819    }
 820
 821    @Override
 822    public boolean onKeyUp(int keyCode, KeyEvent event) {
 823        if (keyCode == KeyEvent.KEYCODE_SEARCH && !event.isLongPress()) {
 824            openSearch();
 825            return true;
 826        }
 827        int c = event.getUnicodeChar();
 828        if (c > 32) {
 829            if (mSearchEditText != null && !mSearchEditText.isFocused()) {
 830                openSearch();
 831                mSearchEditText.append(Character.toString((char) c));
 832                return true;
 833            }
 834        }
 835        return super.onKeyUp(keyCode, event);
 836    }
 837
 838    private void openSearch() {
 839        if (mMenuSearchView != null) {
 840            mMenuSearchView.expandActionView();
 841        }
 842    }
 843
 844    @Override
 845    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
 846        if (resultCode == RESULT_OK) {
 847            if (xmppConnectionServiceBound) {
 848                this.mPostponedActivityResult = null;
 849                if (requestCode == REQUEST_CREATE_CONFERENCE) {
 850                    Account account = extractAccount(intent);
 851                    final String name =
 852                            intent.getStringExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME);
 853                    final List<Jid> jids = ChooseContactActivity.extractJabberIds(intent);
 854                    if (account != null && jids.size() > 0) {
 855                        if (xmppConnectionService.createAdhocConference(
 856                                account, name, jids, mAdhocConferenceCallback)) {
 857                            mToast =
 858                                    Toast.makeText(
 859                                            this, R.string.creating_conference, Toast.LENGTH_LONG);
 860                            mToast.show();
 861                        }
 862                    }
 863                }
 864            } else {
 865                this.mPostponedActivityResult = new Pair<>(requestCode, intent);
 866            }
 867        }
 868        super.onActivityResult(requestCode, requestCode, intent);
 869    }
 870
 871    private boolean askForContactsPermissions() {
 872        if (!QuickConversationsService.isContactListIntegration(this)) {
 873            return false;
 874        }
 875        if (checkSelfPermission(Manifest.permission.READ_CONTACTS)
 876                == PackageManager.PERMISSION_GRANTED) {
 877            return false;
 878        }
 879        if (mRequestedContactsPermission.compareAndSet(false, true)) {
 880            final ImmutableList.Builder<String> permissionBuilder = new ImmutableList.Builder<>();
 881            permissionBuilder.add(Manifest.permission.READ_CONTACTS);
 882            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
 883                permissionBuilder.add(Manifest.permission.POST_NOTIFICATIONS);
 884            }
 885            final String[] permission = permissionBuilder.build().toArray(new String[0]);
 886            final String consent =
 887                    PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
 888                            .getString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, null);
 889            final boolean requiresConsent =
 890                    (QuickConversationsService.isQuicksy()
 891                                    || QuickConversationsService.isPlayStoreFlavor())
 892                            && !"agreed".equals(consent);
 893            if (requiresConsent && "declined".equals(consent)) {
 894                Log.d(
 895                        Config.LOGTAG,
 896                        "not asking for contacts permission because consent has been declined");
 897                return false;
 898            }
 899            if (requiresConsent
 900                    || shouldShowRequestPermissionRationale(Manifest.permission.READ_CONTACTS)) {
 901                final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
 902                final AtomicBoolean requestPermission = new AtomicBoolean(false);
 903                if (QuickConversationsService.isQuicksy()) {
 904                    builder.setTitle(R.string.quicksy_wants_your_consent);
 905                    builder.setMessage(
 906                            Html.fromHtml(getString(R.string.sync_with_contacts_quicksy_static)));
 907                } else {
 908                    builder.setTitle(R.string.sync_with_contacts);
 909                    builder.setMessage(
 910                            getString(
 911                                    R.string.sync_with_contacts_long,
 912                                    getString(R.string.app_name)));
 913                }
 914                @StringRes int confirmButtonText;
 915                if (requiresConsent) {
 916                    confirmButtonText = R.string.agree_and_continue;
 917                } else {
 918                    confirmButtonText = R.string.next;
 919                }
 920                builder.setPositiveButton(
 921                        confirmButtonText,
 922                        (dialog, which) -> {
 923                            if (requiresConsent) {
 924                                PreferenceManager.getDefaultSharedPreferences(
 925                                                getApplicationContext())
 926                                        .edit()
 927                                        .putString(PREF_KEY_CONTACT_INTEGRATION_CONSENT, "agreed")
 928                                        .apply();
 929                            }
 930                            if (requestPermission.compareAndSet(false, true)) {
 931                                requestPermissions(permission, REQUEST_SYNC_CONTACTS);
 932                            }
 933                        });
 934                if (requiresConsent) {
 935                    builder.setNegativeButton(
 936                            R.string.decline,
 937                            (dialog, which) ->
 938                                    PreferenceManager.getDefaultSharedPreferences(
 939                                                    getApplicationContext())
 940                                            .edit()
 941                                            .putString(
 942                                                    PREF_KEY_CONTACT_INTEGRATION_CONSENT,
 943                                                    "declined")
 944                                            .apply());
 945                } else {
 946                    builder.setOnDismissListener(
 947                            dialog -> {
 948                                if (requestPermission.compareAndSet(false, true)) {
 949                                    requestPermissions(permission, REQUEST_SYNC_CONTACTS);
 950                                }
 951                            });
 952                }
 953                builder.setCancelable(requiresConsent);
 954                final AlertDialog dialog = builder.create();
 955                dialog.setCanceledOnTouchOutside(requiresConsent);
 956                dialog.setOnShowListener(
 957                        dialogInterface -> {
 958                            final TextView tv = dialog.findViewById(android.R.id.message);
 959                            if (tv != null) {
 960                                tv.setMovementMethod(LinkMovementMethod.getInstance());
 961                            }
 962                        });
 963                dialog.show();
 964            } else {
 965                requestPermissions(permission, REQUEST_SYNC_CONTACTS);
 966            }
 967        }
 968        return true;
 969    }
 970
 971    @Override
 972    public void onRequestPermissionsResult(
 973            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
 974        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
 975        if (grantResults.length > 0)
 976            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
 977                UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
 978                if (requestCode == REQUEST_SYNC_CONTACTS && xmppConnectionServiceBound) {
 979                    if (QuickConversationsService.isQuicksy()) {
 980                        setRefreshing(true);
 981                    }
 982                    xmppConnectionService.loadPhoneContacts();
 983                    xmppConnectionService.startContactObserver();
 984                }
 985            }
 986    }
 987
 988    private void configureHomeButton() {
 989        final ActionBar actionBar = getSupportActionBar();
 990        if (actionBar == null) {
 991            return;
 992        }
 993        boolean openConversations =
 994                !createdByViewIntent && !xmppConnectionService.isConversationsListEmpty(null);
 995        actionBar.setDisplayHomeAsUpEnabled(openConversations);
 996        actionBar.setDisplayHomeAsUpEnabled(openConversations);
 997    }
 998
 999    @Override
1000    protected void onBackendConnected() {
1001        if (QuickConversationsService.isContactListIntegration(this)
1002                && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
1003                        || checkSelfPermission(Manifest.permission.READ_CONTACTS)
1004                                == PackageManager.PERMISSION_GRANTED)) {
1005            xmppConnectionService.getQuickConversationsService().considerSyncBackground(false);
1006        }
1007        if (mPostponedActivityResult != null) {
1008            onActivityResult(
1009                    mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
1010            this.mPostponedActivityResult = null;
1011        }
1012        this.mActivatedAccounts.clear();
1013        this.mActivatedAccounts.addAll(AccountUtils.getEnabledAccounts(xmppConnectionService));
1014        configureHomeButton();
1015        Intent intent = pendingViewIntent.pop();
1016        if (intent != null && processViewIntent(intent)) {
1017            filter(null);
1018        } else {
1019            if (mSearchEditText != null) {
1020                filter(mSearchEditText.getText().toString());
1021            } else {
1022                filter(null);
1023            }
1024        }
1025        Fragment fragment = getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG_DIALOG);
1026        if (fragment instanceof OnBackendConnected) {
1027            Log.d(Config.LOGTAG, "calling on backend connected on dialog");
1028            ((OnBackendConnected) fragment).onBackendConnected();
1029        }
1030        if (QuickConversationsService.isQuicksy()) {
1031            setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing());
1032        }
1033        if (QuickConversationsService.isConversations()
1034                && AccountUtils.hasEnabledAccounts(xmppConnectionService)
1035                && this.contacts.size() == 0
1036                && this.conferences.size() == 0
1037                && mOpenedFab.compareAndSet(false, true)) {
1038            binding.speedDial.open();
1039        }
1040    }
1041
1042    protected boolean processViewIntent(@NonNull Intent intent) {
1043        final String inviteUri = intent.getStringExtra(EXTRA_INVITE_URI);
1044        if (inviteUri != null) {
1045            final Invite invite = new Invite(inviteUri);
1046            invite.account = intent.getStringExtra(EXTRA_ACCOUNT);
1047            if (invite.isValidJid()) {
1048                return invite.invite();
1049            }
1050        }
1051        final String action = intent.getAction();
1052        if (action == null) {
1053            return false;
1054        }
1055        switch (action) {
1056            case Intent.ACTION_SENDTO:
1057            case Intent.ACTION_VIEW:
1058                Uri uri = intent.getData();
1059                if (uri != null) {
1060                    Invite invite =
1061                            new Invite(intent.getData(), intent.getBooleanExtra("scanned", false));
1062                    invite.account = intent.getStringExtra(EXTRA_ACCOUNT);
1063                    invite.forceDialog = intent.getBooleanExtra("force_dialog", false);
1064                    return invite.invite();
1065                } else {
1066                    return false;
1067                }
1068        }
1069        return false;
1070    }
1071
1072    private boolean handleJid(Invite invite) {
1073        List<Contact> contacts =
1074                xmppConnectionService.findContacts(invite.getJid(), invite.account);
1075        if (invite.isAction(XmppUri.ACTION_JOIN)) {
1076            Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid());
1077            if (muc != null && !invite.forceDialog) {
1078                switchToConversationDoNotAppend(muc, invite.getBody());
1079                return true;
1080            } else {
1081                showJoinConferenceDialog(invite.getJid().asBareJid().toEscapedString());
1082                return false;
1083            }
1084        } else if (contacts.size() == 0) {
1085            showCreateContactDialog(invite.getJid().toEscapedString(), invite);
1086            return false;
1087        } else if (contacts.size() == 1) {
1088            Contact contact = contacts.get(0);
1089            if (!invite.isSafeSource() && invite.hasFingerprints()) {
1090                displayVerificationWarningDialog(contact, invite);
1091            } else {
1092                if (invite.hasFingerprints()) {
1093                    if (xmppConnectionService.verifyFingerprints(
1094                            contact, invite.getFingerprints())) {
1095                        Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT)
1096                                .show();
1097                    }
1098                }
1099                if (invite.account != null) {
1100                    xmppConnectionService.getShortcutService().report(contact);
1101                }
1102                switchToConversationDoNotAppend(contact, invite.getBody());
1103            }
1104            return true;
1105        } else {
1106            if (mMenuSearchView != null) {
1107                mMenuSearchView.expandActionView();
1108                mSearchEditText.setText("");
1109                mSearchEditText.append(invite.getJid().toEscapedString());
1110                filter(invite.getJid().toEscapedString());
1111            } else {
1112                mInitialSearchValue.push(invite.getJid().toEscapedString());
1113            }
1114            return true;
1115        }
1116    }
1117
1118    private void displayVerificationWarningDialog(final Contact contact, final Invite invite) {
1119        final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1120        builder.setTitle(R.string.verify_omemo_keys);
1121        View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
1122        final CheckBox isTrustedSource = view.findViewById(R.id.trusted_source);
1123        TextView warning = view.findViewById(R.id.warning);
1124        warning.setText(
1125                JidDialog.style(
1126                        this,
1127                        R.string.verifying_omemo_keys_trusted_source,
1128                        contact.getJid().asBareJid().toEscapedString(),
1129                        contact.getDisplayName()));
1130        builder.setView(view);
1131        builder.setPositiveButton(
1132                R.string.confirm,
1133                (dialog, which) -> {
1134                    if (isTrustedSource.isChecked() && invite.hasFingerprints()) {
1135                        xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
1136                    }
1137                    switchToConversationDoNotAppend(contact, invite.getBody());
1138                });
1139        builder.setNegativeButton(
1140                R.string.cancel, (dialog, which) -> StartConversationActivity.this.finish());
1141        AlertDialog dialog = builder.create();
1142        dialog.setCanceledOnTouchOutside(false);
1143        dialog.setOnCancelListener(dialog1 -> StartConversationActivity.this.finish());
1144        dialog.show();
1145    }
1146
1147    protected void filter(String needle) {
1148        if (xmppConnectionServiceBound) {
1149            this.filterContacts(needle);
1150            this.filterConferences(needle);
1151        }
1152    }
1153
1154    protected void filterContacts(String needle) {
1155        this.contacts.clear();
1156        final List<Account> accounts = xmppConnectionService.getAccounts();
1157        for (final Account account : accounts) {
1158            if (account.isEnabled()) {
1159                for (Contact contact : account.getRoster().getContacts()) {
1160                    Presence.Status s = contact.getShownStatus();
1161                    if (contact.showInContactList()
1162                            && contact.match(this, needle)
1163                            && (!this.mHideOfflineContacts
1164                                    || (needle != null && !needle.trim().isEmpty())
1165                                    || s.compareTo(Presence.Status.OFFLINE) < 0)) {
1166                        this.contacts.add(contact);
1167                    }
1168                }
1169            }
1170        }
1171        Collections.sort(this.contacts);
1172        mContactsAdapter.notifyDataSetChanged();
1173    }
1174
1175    protected void filterConferences(String needle) {
1176        this.conferences.clear();
1177        for (final Account account : xmppConnectionService.getAccounts()) {
1178            if (account.isEnabled()) {
1179                for (final Bookmark bookmark : account.getBookmarks()) {
1180                    if (bookmark.match(this, needle)) {
1181                        this.conferences.add(bookmark);
1182                    }
1183                }
1184            }
1185        }
1186        Collections.sort(this.conferences);
1187        mConferenceAdapter.notifyDataSetChanged();
1188    }
1189
1190    @Override
1191    public void OnUpdateBlocklist(final Status status) {
1192        refreshUi();
1193    }
1194
1195    @Override
1196    protected void refreshUiReal() {
1197        if (mSearchEditText != null) {
1198            filter(mSearchEditText.getText().toString());
1199        }
1200        configureHomeButton();
1201        if (QuickConversationsService.isQuicksy()) {
1202            setRefreshing(xmppConnectionService.getQuickConversationsService().isSynchronizing());
1203        }
1204    }
1205
1206    @Override
1207    public void onBackPressed() {
1208        if (binding.speedDial.isOpen()) {
1209            binding.speedDial.close();
1210            return;
1211        }
1212        navigateBack();
1213    }
1214
1215    private void navigateBack() {
1216        if (!createdByViewIntent
1217                && xmppConnectionService != null
1218                && !xmppConnectionService.isConversationsListEmpty(null)) {
1219            Intent intent = new Intent(this, ConversationsActivity.class);
1220            intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
1221            startActivity(intent);
1222        }
1223        finish();
1224    }
1225
1226    @Override
1227    public void onCreateDialogPositiveClick(AutoCompleteTextView spinner, String name) {
1228        if (!xmppConnectionServiceBound) {
1229            return;
1230        }
1231        final Account account = getSelectedAccount(this, spinner);
1232        if (account == null) {
1233            return;
1234        }
1235        Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class);
1236        intent.putExtra(ChooseContactActivity.EXTRA_SHOW_ENTER_JID, false);
1237        intent.putExtra(ChooseContactActivity.EXTRA_SELECT_MULTIPLE, true);
1238        intent.putExtra(ChooseContactActivity.EXTRA_GROUP_CHAT_NAME, name.trim());
1239        intent.putExtra(
1240                ChooseContactActivity.EXTRA_ACCOUNT,
1241                account.getJid().asBareJid().toEscapedString());
1242        intent.putExtra(ChooseContactActivity.EXTRA_TITLE_RES_ID, R.string.choose_participants);
1243        startActivityForResult(intent, REQUEST_CREATE_CONFERENCE);
1244    }
1245
1246    @Override
1247    public void onJoinDialogPositiveClick(
1248            final Dialog dialog,
1249            final AutoCompleteTextView spinner,
1250            final TextInputLayout layout,
1251            final AutoCompleteTextView jid) {
1252        if (!xmppConnectionServiceBound) {
1253            return;
1254        }
1255        final Account account = getSelectedAccount(this, spinner);
1256        if (account == null) {
1257            return;
1258        }
1259        final String input = jid.getText().toString().trim();
1260        Jid conferenceJid;
1261        try {
1262            conferenceJid = Jid.ofEscaped(input);
1263        } catch (final IllegalArgumentException e) {
1264            final XmppUri xmppUri = new XmppUri(input);
1265            if (xmppUri.isValidJid() && xmppUri.isAction(XmppUri.ACTION_JOIN)) {
1266                final Editable editable = jid.getEditableText();
1267                editable.clear();
1268                editable.append(xmppUri.getJid().toEscapedString());
1269                conferenceJid = xmppUri.getJid();
1270            } else {
1271                layout.setError(getString(R.string.invalid_jid));
1272                return;
1273            }
1274        }
1275        final var existingBookmark = account.getBookmark(conferenceJid);
1276        if (existingBookmark != null) {
1277            openConversationsForBookmark(existingBookmark);
1278        } else {
1279            final var bookmark = new Bookmark(account, conferenceJid.asBareJid());
1280            bookmark.setAutojoin(true);
1281            final String nick = conferenceJid.getResource();
1282            if (nick != null && !nick.isEmpty() && !nick.equals(MucOptions.defaultNick(account))) {
1283                bookmark.setNick(nick);
1284            }
1285            xmppConnectionService.createBookmark(account, bookmark);
1286            final Conversation conversation =
1287                    xmppConnectionService.findOrCreateConversation(
1288                            account, conferenceJid, true, true, true);
1289            bookmark.setConversation(conversation);
1290            switchToConversation(conversation);
1291        }
1292        dialog.dismiss();
1293    }
1294
1295    @Override
1296    public void onConversationUpdate() {
1297        refreshUi();
1298    }
1299
1300    @Override
1301    public void onRefresh() {
1302        Log.d(Config.LOGTAG, "user requested to refresh");
1303        if (QuickConversationsService.isQuicksy() && xmppConnectionService != null) {
1304            xmppConnectionService.getQuickConversationsService().considerSyncBackground(true);
1305        }
1306    }
1307
1308    private void setRefreshing(boolean refreshing) {
1309        MyListFragment fragment = (MyListFragment) mListPagerAdapter.getItem(0);
1310        if (fragment != null) {
1311            fragment.setRefreshing(refreshing);
1312        }
1313    }
1314
1315    @Override
1316    public void onCreatePublicChannel(Account account, String name, Jid address) {
1317        mToast = Toast.makeText(this, R.string.creating_channel, Toast.LENGTH_LONG);
1318        mToast.show();
1319        xmppConnectionService.createPublicChannel(
1320                account,
1321                name,
1322                address,
1323                new UiCallback<Conversation>() {
1324                    @Override
1325                    public void success(Conversation conversation) {
1326                        runOnUiThread(
1327                                () -> {
1328                                    hideToast();
1329                                    switchToConversation(conversation);
1330                                });
1331                    }
1332
1333                    @Override
1334                    public void error(int errorCode, Conversation conversation) {
1335                        runOnUiThread(
1336                                () -> {
1337                                    replaceToast(getString(errorCode));
1338                                    switchToConversation(conversation);
1339                                });
1340                    }
1341
1342                    @Override
1343                    public void userInputRequired(PendingIntent pi, Conversation object) {}
1344                });
1345    }
1346
1347    public static class MyListFragment extends SwipeRefreshListFragment {
1348        private AdapterView.OnItemClickListener mOnItemClickListener;
1349        private int mResContextMenu;
1350
1351        public void setContextMenu(final int res) {
1352            this.mResContextMenu = res;
1353        }
1354
1355        @Override
1356        public void onListItemClick(
1357                final ListView l, final View v, final int position, final long id) {
1358            if (mOnItemClickListener != null) {
1359                mOnItemClickListener.onItemClick(l, v, position, id);
1360            }
1361        }
1362
1363        public void setOnListItemClickListener(AdapterView.OnItemClickListener l) {
1364            this.mOnItemClickListener = l;
1365        }
1366
1367        @Override
1368        public void onViewCreated(@NonNull final View view, final Bundle savedInstanceState) {
1369            super.onViewCreated(view, savedInstanceState);
1370            registerForContextMenu(getListView());
1371            getListView().setFastScrollEnabled(true);
1372            getListView().setDivider(null);
1373            getListView().setDividerHeight(0);
1374        }
1375
1376        @Override
1377        public void onCreateContextMenu(
1378                @NonNull final ContextMenu menu,
1379                @NonNull final View v,
1380                final ContextMenuInfo menuInfo) {
1381            super.onCreateContextMenu(menu, v, menuInfo);
1382            final StartConversationActivity activity = (StartConversationActivity) getActivity();
1383            if (activity == null) {
1384                return;
1385            }
1386            activity.getMenuInflater().inflate(mResContextMenu, menu);
1387            final AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
1388            if (mResContextMenu == R.menu.conference_context) {
1389                activity.conference_context_id = acmi.position;
1390                final Bookmark bookmark = (Bookmark) activity.conferences.get(acmi.position);
1391                final Conversation conversation = bookmark.getConversation();
1392                final MenuItem share = menu.findItem(R.id.context_share_uri);
1393                final MenuItem delete = menu.findItem(R.id.context_delete_conference);
1394                if (conversation != null) {
1395                    delete.setTitle(R.string.delete_and_close);
1396                } else {
1397                    delete.setTitle(R.string.delete_bookmark);
1398                }
1399                share.setVisible(conversation == null || !conversation.isPrivateAndNonAnonymous());
1400            } else if (mResContextMenu == R.menu.contact_context) {
1401                activity.contact_context_id = acmi.position;
1402                final Contact contact = (Contact) activity.contacts.get(acmi.position);
1403                final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock);
1404                final MenuItem showContactDetailsItem = menu.findItem(R.id.context_contact_details);
1405                final MenuItem deleteContactMenuItem = menu.findItem(R.id.context_delete_contact);
1406                if (contact.isSelf()) {
1407                    showContactDetailsItem.setVisible(false);
1408                }
1409                deleteContactMenuItem.setVisible(
1410                        contact.showInRoster()
1411                                && !contact.getOption(Contact.Options.SYNCED_VIA_OTHER));
1412                final XmppConnection xmpp = contact.getAccount().getXmppConnection();
1413                if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) {
1414                    if (contact.isBlocked()) {
1415                        blockUnblockItem.setTitle(R.string.unblock_contact);
1416                    } else {
1417                        blockUnblockItem.setTitle(R.string.block_contact);
1418                    }
1419                } else {
1420                    blockUnblockItem.setVisible(false);
1421                }
1422            }
1423        }
1424
1425        @Override
1426        public boolean onContextItemSelected(final MenuItem item) {
1427            StartConversationActivity activity = (StartConversationActivity) getActivity();
1428            if (activity == null) {
1429                return true;
1430            }
1431            switch (item.getItemId()) {
1432                case R.id.context_contact_details:
1433                    activity.openDetailsForContact();
1434                    break;
1435                case R.id.context_show_qr:
1436                    activity.showQrForContact();
1437                    break;
1438                case R.id.context_contact_block_unblock:
1439                    activity.toggleContactBlock();
1440                    break;
1441                case R.id.context_delete_contact:
1442                    activity.deleteContact();
1443                    break;
1444                case R.id.context_share_uri:
1445                    activity.shareBookmarkUri();
1446                    break;
1447                case R.id.context_delete_conference:
1448                    activity.deleteConference();
1449            }
1450            return true;
1451        }
1452    }
1453
1454    public class ListPagerAdapter extends PagerAdapter {
1455        private final FragmentManager fragmentManager;
1456        private final MyListFragment[] fragments;
1457
1458        ListPagerAdapter(FragmentManager fm) {
1459            fragmentManager = fm;
1460            fragments = new MyListFragment[2];
1461        }
1462
1463        public void requestFocus(int pos) {
1464            if (fragments.length > pos) {
1465                fragments[pos].getListView().requestFocus();
1466            }
1467        }
1468
1469        @Override
1470        public void destroyItem(
1471                @NonNull ViewGroup container, int position, @NonNull Object object) {
1472            FragmentTransaction trans = fragmentManager.beginTransaction();
1473            trans.remove(fragments[position]);
1474            trans.commit();
1475            fragments[position] = null;
1476        }
1477
1478        @NonNull
1479        @Override
1480        public Fragment instantiateItem(@NonNull ViewGroup container, int position) {
1481            final Fragment fragment = getItem(position);
1482            final FragmentTransaction trans = fragmentManager.beginTransaction();
1483            trans.add(container.getId(), fragment, "fragment:" + position);
1484            try {
1485                trans.commit();
1486            } catch (IllegalStateException e) {
1487                // ignore
1488            }
1489            return fragment;
1490        }
1491
1492        @Override
1493        public int getCount() {
1494            return fragments.length;
1495        }
1496
1497        @Override
1498        public boolean isViewFromObject(@NonNull View view, @NonNull Object fragment) {
1499            return ((Fragment) fragment).getView() == view;
1500        }
1501
1502        @Nullable
1503        @Override
1504        public CharSequence getPageTitle(int position) {
1505            switch (position) {
1506                case 0:
1507                    return getResources().getString(R.string.contacts);
1508                case 1:
1509                    return getResources().getString(R.string.group_chats);
1510                default:
1511                    return super.getPageTitle(position);
1512            }
1513        }
1514
1515        Fragment getItem(int position) {
1516            if (fragments[position] == null) {
1517                final MyListFragment listFragment = new MyListFragment();
1518                if (position == 1) {
1519                    listFragment.setListAdapter(mConferenceAdapter);
1520                    listFragment.setContextMenu(R.menu.conference_context);
1521                    listFragment.setOnListItemClickListener(
1522                            (arg0, arg1, p, arg3) -> openConversationForBookmark(p));
1523                } else {
1524                    listFragment.setListAdapter(mContactsAdapter);
1525                    listFragment.setContextMenu(R.menu.contact_context);
1526                    listFragment.setOnListItemClickListener(
1527                            (arg0, arg1, p, arg3) -> openConversationForContact(p));
1528                    if (QuickConversationsService.isQuicksy()) {
1529                        listFragment.setOnRefreshListener(StartConversationActivity.this);
1530                    }
1531                }
1532                fragments[position] = listFragment;
1533            }
1534            return fragments[position];
1535        }
1536    }
1537
1538    public static void addInviteUri(Intent to, Intent from) {
1539        if (from != null && from.hasExtra(EXTRA_INVITE_URI)) {
1540            final String invite = from.getStringExtra(EXTRA_INVITE_URI);
1541            to.putExtra(EXTRA_INVITE_URI, invite);
1542        }
1543    }
1544
1545    private class Invite extends XmppUri {
1546
1547        public String account;
1548
1549        boolean forceDialog = false;
1550
1551        Invite(final String uri) {
1552            super(uri);
1553        }
1554
1555        Invite(Uri uri, boolean safeSource) {
1556            super(uri, safeSource);
1557        }
1558
1559        boolean invite() {
1560            if (!isValidJid()) {
1561                Toast.makeText(
1562                                StartConversationActivity.this,
1563                                R.string.invalid_jid,
1564                                Toast.LENGTH_SHORT)
1565                        .show();
1566                return false;
1567            }
1568            if (getJid() != null) {
1569                return handleJid(this);
1570            }
1571            return false;
1572        }
1573    }
1574}