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